From 7934880496d70850c04a3c0ec7f41c68cebd9492 Mon Sep 17 00:00:00 2001 From: Stavros Kois <47820033+stavros-k@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:40:35 +0200 Subject: [PATCH] apps: bump library (#1222) --- ix-dev/community/actual-budget/app.yaml | 6 +- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../tests/test_validations.py | 6 + .../tests/test_volumes.py | 0 .../library/base_v2_1_6}/validations.py | 8 +- .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6}/volume_sources.py | 2 +- .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/adguard-home/app.yaml | 6 +- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6}/tests/test_validations.py | 6 + .../tests/test_volumes.py | 0 .../library/base_v2_1_6}/validations.py | 8 +- .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../volume_sources.py | 2 +- .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/audiobookshelf/app.yaml | 6 +- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6}/tests/test_validations.py | 6 + .../tests/test_volumes.py | 0 .../validations.py | 8 +- .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6}/volume_sources.py | 2 +- .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/autobrr/app.yaml | 6 +- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6}/tests/test_validations.py | 6 + .../tests/test_volumes.py | 0 .../validations.py | 8 +- .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../volume_sources.py | 2 +- .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/bazarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/briefkasten/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/calibre-web/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/calibre/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/castopod/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/chia/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/clamav/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/cloudflared/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/dashy/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/ddns-updater/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 .../{v1_1_3 => v1_1_4}/__init__.py | 0 .../ddns-updater/{v1_1_3 => v1_1_4}/config.py | 0 ix-dev/community/deluge/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/distribution/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/dockge/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/drawio/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/eclipse-mosquitto/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/filebrowser/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/filestash/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/firefly-iii/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/flame/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/flaresolverr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/freshrss/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/frigate/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/fscrawler/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/gaseous-server/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/gitea/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/grafana/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/handbrake/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/homarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/homepage/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/homer/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 .../community/iconik-storage-gateway/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/immich/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/invidious/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/ipfs/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/jellyfin/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/jellyseerr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/jenkins/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/joplin/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/kapowarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/kavita/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/komga/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/lidarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/linkding/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/listmonk/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/logseq/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/mealie/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/metube/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/minecraft/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/mineos/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/mumble/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/n8n/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/navidrome/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/netbootxyz/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/nginx-proxy-manager/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/node-red/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/odoo/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/ollama/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/omada-controller/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/open-webui/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/organizr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/overseerr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/palworld/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/paperless-ngx/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/passbolt/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/penpot/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/pgadmin/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/pigallery2/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/piwigo/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/planka/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/plex-auto-languages/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/portainer/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/postgres/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/prowlarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/qbittorrent/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/radarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/readarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/recyclarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/redis/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/roundcube/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/rsyncd/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/rust-desk/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/sabnzbd/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/scrutiny/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/searxng/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/sftpgo/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/sonarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/tailscale/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/tautulli/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/tdarr/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/terraria/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/tftpd-hpa/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/tiny-media-manager/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/transmission/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/twofactor-auth/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/unifi-controller/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 .../community/unifi-protect-backup/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/uptime-kuma/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/vaultwarden/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/vikunja/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/webdav/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/whoogle/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/wordpress/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/community/zerotier/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/enterprise/asigra-ds-system/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/enterprise/minio/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/enterprise/syncthing/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/collabora/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/diskoverdata/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/elastic-search/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/emby/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/home-assistant/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/ix-app/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/minio/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/netdata/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/nextcloud/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/photoprism/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/pihole/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/plex/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/prometheus/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/storj/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/syncthing/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 ix-dev/stable/wg-easy/app.yaml | 6 +- .../base_v2_1_5/tests/test_validations.py | 126 -------- .../library/base_v2_1_5/validations.py | 271 ------------------ .../library/base_v2_1_5/volume_sources.py | 108 ------- .../{base_v2_1_5 => base_v2_1_6}/__init__.py | 0 .../{base_v2_1_5 => base_v2_1_6}/configs.py | 0 .../{base_v2_1_5 => base_v2_1_6}/container.py | 0 .../{base_v2_1_5 => base_v2_1_6}/depends.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deploy.py | 0 .../{base_v2_1_5 => base_v2_1_6}/deps.py | 0 .../deps_mariadb.py | 0 .../deps_perms.py | 0 .../deps_postgres.py | 0 .../deps_redis.py | 0 .../{base_v2_1_5 => base_v2_1_6}/device.py | 0 .../{base_v2_1_5 => base_v2_1_6}/devices.py | 0 .../{base_v2_1_5 => base_v2_1_6}/dns.py | 0 .../environment.py | 0 .../{base_v2_1_5 => base_v2_1_6}/error.py | 0 .../{base_v2_1_5 => base_v2_1_6}/formatter.py | 0 .../{base_v2_1_5 => base_v2_1_6}/functions.py | 0 .../healthcheck.py | 0 .../{base_v2_1_5 => base_v2_1_6}/labels.py | 0 .../{base_v2_1_5 => base_v2_1_6}/notes.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portal.py | 0 .../{base_v2_1_5 => base_v2_1_6}/portals.py | 0 .../{base_v2_1_5 => base_v2_1_6}/ports.py | 0 .../{base_v2_1_5 => base_v2_1_6}/render.py | 0 .../{base_v2_1_5 => base_v2_1_6}/resources.py | 0 .../{base_v2_1_5 => base_v2_1_6}/restart.py | 0 .../{base_v2_1_5 => base_v2_1_6}/storage.py | 0 .../{base_v2_1_5 => base_v2_1_6}/sysctls.py | 0 .../tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../tests/test_configs.py | 0 .../tests/test_container.py | 0 .../tests/test_depends.py | 0 .../tests/test_deps.py | 0 .../tests/test_device.py | 0 .../tests/test_dns.py | 0 .../tests/test_environment.py | 0 .../tests/test_formatter.py | 0 .../tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 .../tests/test_labels.py | 0 .../tests/test_notes.py | 0 .../tests/test_portal.py | 0 .../tests/test_ports.py | 0 .../tests/test_render.py | 0 .../tests/test_resources.py | 0 .../tests/test_restart.py | 0 .../tests/test_sysctls.py | 0 .../base_v2_1_6/tests/test_validations.py | 132 +++++++++ .../tests/test_volumes.py | 0 .../library/base_v2_1_6/validations.py | 271 ++++++++++++++++++ .../volume_mount.py | 0 .../volume_mount_types.py | 0 .../library/base_v2_1_6/volume_sources.py | 108 +++++++ .../volume_types.py | 0 .../{base_v2_1_5 => base_v2_1_6}/volumes.py | 0 7550 files changed, 62764 insertions(+), 62008 deletions(-) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_validations.py (95%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) rename ix-dev/community/{adguard-home/templates/library/base_v2_1_5 => actual-budget/templates/library/base_v2_1_6}/validations.py (96%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) rename ix-dev/community/{audiobookshelf/templates/library/base_v2_1_5 => actual-budget/templates/library/base_v2_1_6}/volume_sources.py (98%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/actual-budget/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) rename ix-dev/community/{audiobookshelf/templates/library/base_v2_1_5 => adguard-home/templates/library/base_v2_1_6}/tests/test_validations.py (95%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) rename ix-dev/community/{actual-budget/templates/library/base_v2_1_5 => adguard-home/templates/library/base_v2_1_6}/validations.py (96%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_sources.py (98%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/adguard-home/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) rename ix-dev/community/{autobrr/templates/library/base_v2_1_5 => audiobookshelf/templates/library/base_v2_1_6}/tests/test_validations.py (95%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/validations.py (96%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) rename ix-dev/community/{actual-budget/templates/library/base_v2_1_5 => audiobookshelf/templates/library/base_v2_1_6}/volume_sources.py (98%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/audiobookshelf/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) rename ix-dev/community/{adguard-home/templates/library/base_v2_1_5 => autobrr/templates/library/base_v2_1_6}/tests/test_validations.py (95%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/validations.py (96%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_sources.py (98%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/autobrr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/bazarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_6/validations.py rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/briefkasten/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_6/validations.py rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/calibre-web/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_6/validations.py rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/calibre/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/calibre/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_6/validations.py rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/castopod/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/castopod/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/chia/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/chia/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/chia/templates/library/base_v2_1_6/validations.py rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/chia/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/chia/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_6/validations.py rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/clamav/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/clamav/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_6/validations.py rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/cloudflared/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_6/validations.py rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/dashy/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/dashy/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_6/validations.py rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/ddns-updater/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) rename ix-dev/community/ddns-updater/templates/library/community/ddns-updater/{v1_1_3 => v1_1_4}/__init__.py (100%) rename ix-dev/community/ddns-updater/templates/library/community/ddns-updater/{v1_1_3 => v1_1_4}/config.py (100%) delete mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_6/validations.py rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/deluge/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/deluge/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_6/validations.py rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/distribution/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/distribution/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_6/validations.py rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/dockge/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/dockge/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_6/validations.py rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/drawio/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/drawio/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/validations.py rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/eclipse-mosquitto/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_6/validations.py rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/filebrowser/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_6/validations.py rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/filestash/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/filestash/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_6/validations.py rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/firefly-iii/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/flame/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/flame/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/flame/templates/library/base_v2_1_6/validations.py rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/flame/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/flame/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/flaresolverr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_6/validations.py rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/freshrss/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_6/validations.py rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/frigate/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/frigate/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_6/validations.py rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/fscrawler/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_6/validations.py rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/gaseous-server/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_6/validations.py rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/gitea/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/gitea/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_6/validations.py rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/grafana/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/grafana/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_6/validations.py rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/handbrake/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/homarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/homarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_6/validations.py rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/homepage/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/homepage/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/homer/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/homer/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/homer/templates/library/base_v2_1_6/validations.py rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/homer/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/homer/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/validations.py rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/iconik-storage-gateway/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/immich/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/immich/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/immich/templates/library/base_v2_1_6/validations.py rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/immich/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/immich/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_6/validations.py rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/invidious/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/invidious/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_6/validations.py rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/ipfs/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_6/validations.py rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/jellyfin/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/jellyseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_6/validations.py rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/jenkins/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_6/validations.py rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/joplin/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/joplin/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/kapowarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_6/validations.py rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/kavita/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/kavita/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/komga/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/komga/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/komga/templates/library/base_v2_1_6/validations.py rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/komga/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/komga/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/lidarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_6/validations.py rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/linkding/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/linkding/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_6/validations.py rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/listmonk/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_6/validations.py rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/logseq/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/logseq/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_6/validations.py rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/mealie/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/mealie/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/metube/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/metube/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/metube/templates/library/base_v2_1_6/validations.py rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/metube/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/metube/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_6/validations.py rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/minecraft/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_6/validations.py rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/mineos/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/mineos/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_6/validations.py rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/mumble/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/mumble/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_6/validations.py rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/n8n/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/n8n/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_6/validations.py rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/navidrome/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_6/validations.py rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/netbootxyz/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/validations.py rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/nginx-proxy-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_6/validations.py rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/node-red/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/node-red/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_6/validations.py rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/odoo/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/odoo/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_6/validations.py rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/ollama/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/ollama/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_6/validations.py rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/omada-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_6/validations.py rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/open-webui/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/organizr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/organizr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/overseerr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_6/validations.py rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/palworld/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/palworld/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/validations.py rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/paperless-ngx/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_6/validations.py rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/passbolt/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_6/validations.py rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/penpot/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/penpot/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_6/validations.py rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/pgadmin/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_6/validations.py rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/pigallery2/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_6/validations.py rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/piwigo/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/planka/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/planka/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/planka/templates/library/base_v2_1_6/validations.py rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/planka/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/planka/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/validations.py rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/plex-auto-languages/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_6/validations.py rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/portainer/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/portainer/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_6/validations.py rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/postgres/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/postgres/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/prowlarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_6/validations.py rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/qbittorrent/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/radarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/radarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/readarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/readarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/recyclarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/redis/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/redis/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/redis/templates/library/base_v2_1_6/validations.py rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/redis/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/redis/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_6/validations.py rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/roundcube/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_6/validations.py rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/rsyncd/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_6/validations.py rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/rust-desk/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_6/validations.py rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/sabnzbd/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_6/validations.py rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/scrutiny/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_6/validations.py rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/searxng/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/searxng/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_6/validations.py rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/sftpgo/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/sonarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_6/validations.py rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/tailscale/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_6/validations.py rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/tautulli/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_6/validations.py rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/tdarr/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_6/validations.py rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/terraria/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/terraria/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/validations.py rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/tftpd-hpa/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/validations.py rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/tiny-media-manager/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_6/validations.py rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/transmission/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/transmission/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/validations.py rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/twofactor-auth/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_6/validations.py rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/unifi-controller/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/validations.py rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/unifi-protect-backup/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/validations.py rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/uptime-kuma/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_6/validations.py rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/vaultwarden/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_6/validations.py rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/vikunja/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_6/validations.py rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/webdav/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/webdav/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_6/validations.py rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/whoogle/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_6/validations.py rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/wordpress/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_6/validations.py rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/community/zerotier/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/validations.py rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/enterprise/asigra-ds-system/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_6/validations.py rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/enterprise/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/validations.py rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/enterprise/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/collabora/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/diskoverdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/elastic-search/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/emby/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/emby/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/home-assistant/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/ix-app/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/minio/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/minio/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/netdata/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/nextcloud/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/photoprism/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/pihole/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/plex/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/plex/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/prometheus/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/storj/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/storj/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/syncthing/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) delete mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_validations.py delete mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_5/validations.py delete mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_sources.py rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/__init__.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/configs.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/container.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/depends.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deploy.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_mariadb.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_perms.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_postgres.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/deps_redis.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/device.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/devices.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/dns.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/environment.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/error.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/formatter.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/functions.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/healthcheck.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/labels.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/notes.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/portal.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/portals.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/ports.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/render.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/resources.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/restart.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/storage.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/sysctls.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/__init__.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_build_image.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_configs.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_container.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_depends.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_deps.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_device.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_dns.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_environment.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_formatter.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_functions.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_healthcheck.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_labels.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_notes.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_portal.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_ports.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_render.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_resources.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_restart.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_sysctls.py (100%) create mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_validations.py rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/tests/test_volumes.py (100%) create mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_6/validations.py rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_mount_types.py (100%) create mode 100644 ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_sources.py rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/volume_types.py (100%) rename ix-dev/stable/wg-easy/templates/library/{base_v2_1_5 => base_v2_1_6}/volumes.py (100%) diff --git a/ix-dev/community/actual-budget/app.yaml b/ix-dev/community/actual-budget/app.yaml index 47891807b4..a8463e130c 100644 --- a/ix-dev/community/actual-budget/app.yaml +++ b/ix-dev/community/actual-budget/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/actual-budget/icons/icon.png keywords: - finance - budget -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://hub.docker.com/r/actualbudget/actual-server title: Actual Budget train: community -version: 1.2.6 +version: 1.2.7 diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/configs.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/container.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/container.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/depends.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/device.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/device.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/devices.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/dns.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/environment.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/error.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/error.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/functions.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/labels.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/notes.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/portal.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/portals.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/ports.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/render.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/render.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/resources.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/restart.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/storage.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_validations.py similarity index 95% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_validations.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_validations.py index 27fc0e903b..f0986ce9a5 100644 --- a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_validations.py +++ b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_validations.py @@ -49,6 +49,12 @@ def test_is_allowed_path_direct(test_path, expected): assert is_allowed_path(test_path) == expected +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + @patch.object(Path, "resolve", mock_resolve) def test_is_allowed_path_symlink(tmp_path): """ diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/validations.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/validations.py similarity index 96% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/validations.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/validations.py index 4c7065c1c7..b0a761238f 100644 --- a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/validations.py +++ b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/validations.py @@ -150,7 +150,7 @@ def valid_fs_path_or_raise(path: str): return path -def is_allowed_path(input_path: str) -> bool: +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: """ Validates that the given path (after resolving symlinks) is not one of the restricted paths or within those restricted directories. @@ -159,15 +159,15 @@ def is_allowed_path(input_path: str) -> bool: """ # Resolve the path to avoid symlink bypasses real_path = Path(input_path).resolve() - for restricted in RESTRICTED: + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: if real_path.is_relative_to(restricted): return False return real_path not in RESTRICTED_IN -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): raise RenderError(f"Path [{path}] is not allowed to be mounted.") return path diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_sources.py similarity index 98% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_sources.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_sources.py index 030ccd397b..dcfce44b75 100644 --- a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_sources.py +++ b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_sources.py @@ -58,7 +58,7 @@ def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig") ) path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) + self.source = allowed_fs_host_path_or_raise(path, True) def get(self): return self.source diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/actual-budget/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/actual-budget/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/adguard-home/app.yaml b/ix-dev/community/adguard-home/app.yaml index 0ebe60b100..d37031ddd6 100644 --- a/ix-dev/community/adguard-home/app.yaml +++ b/ix-dev/community/adguard-home/app.yaml @@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/adguard-home/icons/icon.svg keywords: - dns - adblock -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://hub.docker.com/r/adguard/adguardhome title: AdGuard Home train: community -version: 1.1.8 +version: 1.1.9 diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/configs.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/container.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/container.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/depends.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/device.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/device.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/devices.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/dns.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/environment.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/error.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/error.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/functions.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/labels.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/notes.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/portal.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/portals.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/ports.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/render.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/render.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/resources.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/restart.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/storage.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_validations.py similarity index 95% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_validations.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_validations.py index 27fc0e903b..f0986ce9a5 100644 --- a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_validations.py +++ b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_validations.py @@ -49,6 +49,12 @@ def test_is_allowed_path_direct(test_path, expected): assert is_allowed_path(test_path) == expected +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + @patch.object(Path, "resolve", mock_resolve) def test_is_allowed_path_symlink(tmp_path): """ diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/validations.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/validations.py similarity index 96% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/validations.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/validations.py index 4c7065c1c7..b0a761238f 100644 --- a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/validations.py +++ b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/validations.py @@ -150,7 +150,7 @@ def valid_fs_path_or_raise(path: str): return path -def is_allowed_path(input_path: str) -> bool: +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: """ Validates that the given path (after resolving symlinks) is not one of the restricted paths or within those restricted directories. @@ -159,15 +159,15 @@ def is_allowed_path(input_path: str) -> bool: """ # Resolve the path to avoid symlink bypasses real_path = Path(input_path).resolve() - for restricted in RESTRICTED: + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: if real_path.is_relative_to(restricted): return False return real_path not in RESTRICTED_IN -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): raise RenderError(f"Path [{path}] is not allowed to be mounted.") return path diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_sources.py similarity index 98% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_sources.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_sources.py index 030ccd397b..dcfce44b75 100644 --- a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_sources.py +++ b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_sources.py @@ -58,7 +58,7 @@ def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig") ) path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) + self.source = allowed_fs_host_path_or_raise(path, True) def get(self): return self.source diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/adguard-home/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/adguard-home/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/audiobookshelf/app.yaml b/ix-dev/community/audiobookshelf/app.yaml index e77b903669..c6a78c9e1f 100644 --- a/ix-dev/community/audiobookshelf/app.yaml +++ b/ix-dev/community/audiobookshelf/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/audiobookshelf/icons/icon.svg keywords: - media - audiobook -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://github.com/advplyr/audiobookshelf title: Audiobookshelf train: community -version: 1.3.6 +version: 1.3.7 diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/configs.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/container.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/container.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/depends.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/device.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/device.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/devices.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/dns.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/environment.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/error.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/error.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/functions.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/labels.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/notes.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/portal.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/portals.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/ports.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/render.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/render.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/resources.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/restart.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/storage.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_validations.py similarity index 95% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_validations.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_validations.py index 27fc0e903b..f0986ce9a5 100644 --- a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_validations.py +++ b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_validations.py @@ -49,6 +49,12 @@ def test_is_allowed_path_direct(test_path, expected): assert is_allowed_path(test_path) == expected +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + @patch.object(Path, "resolve", mock_resolve) def test_is_allowed_path_symlink(tmp_path): """ diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/validations.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/validations.py similarity index 96% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/validations.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/validations.py index 4c7065c1c7..b0a761238f 100644 --- a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/validations.py +++ b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/validations.py @@ -150,7 +150,7 @@ def valid_fs_path_or_raise(path: str): return path -def is_allowed_path(input_path: str) -> bool: +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: """ Validates that the given path (after resolving symlinks) is not one of the restricted paths or within those restricted directories. @@ -159,15 +159,15 @@ def is_allowed_path(input_path: str) -> bool: """ # Resolve the path to avoid symlink bypasses real_path = Path(input_path).resolve() - for restricted in RESTRICTED: + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: if real_path.is_relative_to(restricted): return False return real_path not in RESTRICTED_IN -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): raise RenderError(f"Path [{path}] is not allowed to be mounted.") return path diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_sources.py similarity index 98% rename from ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_sources.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_sources.py index 030ccd397b..dcfce44b75 100644 --- a/ix-dev/community/actual-budget/templates/library/base_v2_1_5/volume_sources.py +++ b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_sources.py @@ -58,7 +58,7 @@ def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig") ) path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) + self.source = allowed_fs_host_path_or_raise(path, True) def get(self): return self.source diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/audiobookshelf/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/audiobookshelf/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/autobrr/app.yaml b/ix-dev/community/autobrr/app.yaml index 0487782935..861fda8b49 100644 --- a/ix-dev/community/autobrr/app.yaml +++ b/ix-dev/community/autobrr/app.yaml @@ -10,8 +10,8 @@ keywords: - media - torrent - usenet -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/autobrr/autobrr title: Autobrr train: community -version: 1.2.8 +version: 1.2.9 diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/container.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/device.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/error.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/render.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_validations.py similarity index 95% rename from ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_validations.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_validations.py index 27fc0e903b..f0986ce9a5 100644 --- a/ix-dev/community/adguard-home/templates/library/base_v2_1_5/tests/test_validations.py +++ b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_validations.py @@ -49,6 +49,12 @@ def test_is_allowed_path_direct(test_path, expected): assert is_allowed_path(test_path) == expected +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + @patch.object(Path, "resolve", mock_resolve) def test_is_allowed_path_symlink(tmp_path): """ diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/validations.py similarity index 96% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/validations.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/validations.py index 4c7065c1c7..b0a761238f 100644 --- a/ix-dev/community/autobrr/templates/library/base_v2_1_5/validations.py +++ b/ix-dev/community/autobrr/templates/library/base_v2_1_6/validations.py @@ -150,7 +150,7 @@ def valid_fs_path_or_raise(path: str): return path -def is_allowed_path(input_path: str) -> bool: +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: """ Validates that the given path (after resolving symlinks) is not one of the restricted paths or within those restricted directories. @@ -159,15 +159,15 @@ def is_allowed_path(input_path: str) -> bool: """ # Resolve the path to avoid symlink bypasses real_path = Path(input_path).resolve() - for restricted in RESTRICTED: + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: if real_path.is_relative_to(restricted): return False return real_path not in RESTRICTED_IN -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): raise RenderError(f"Path [{path}] is not allowed to be mounted.") return path diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_sources.py similarity index 98% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_sources.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_sources.py index 030ccd397b..dcfce44b75 100644 --- a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_sources.py +++ b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_sources.py @@ -58,7 +58,7 @@ def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig") ) path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) + self.source = allowed_fs_host_path_or_raise(path, True) def get(self): return self.source diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/autobrr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/autobrr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/autobrr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/autobrr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/bazarr/app.yaml b/ix-dev/community/bazarr/app.yaml index 3d6bd38bf3..f90f029eac 100644 --- a/ix-dev/community/bazarr/app.yaml +++ b/ix-dev/community/bazarr/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/bazarr/icons/icon.png keywords: - media - subtitles -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/morpheus65535/bazarr title: Bazarr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/bazarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/bazarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/bazarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/bazarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/bazarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/bazarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/bazarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/briefkasten/app.yaml b/ix-dev/community/briefkasten/app.yaml index a3a1b09cb6..752a2cf753 100644 --- a/ix-dev/community/briefkasten/app.yaml +++ b/ix-dev/community/briefkasten/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/briefkasten/icons/icon.svg keywords: - bookmark -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -36,4 +36,4 @@ sources: - https://docs.briefkastenhq.com/ title: Briefkasten train: community -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/validations.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/configs.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/container.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/container.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/depends.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/device.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/device.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/devices.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/dns.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/environment.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/error.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/error.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/functions.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/labels.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/notes.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/portal.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/portals.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/ports.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/render.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/render.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/resources.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/restart.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/storage.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_6/validations.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/briefkasten/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/briefkasten/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/briefkasten/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/briefkasten/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/calibre-web/app.yaml b/ix-dev/community/calibre-web/app.yaml index b2c10c8b07..cd14496846 100644 --- a/ix-dev/community/calibre-web/app.yaml +++ b/ix-dev/community/calibre-web/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/calibre-web/icons/icon.svg keywords: - media - ebooks -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://github.com/janeczku/calibre-web title: Calibre Web train: community -version: 1.0.6 +version: 1.0.7 diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/validations.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/configs.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/container.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/container.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/depends.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/device.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/device.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/devices.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/dns.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/environment.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/error.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/error.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/functions.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/labels.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/notes.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/portal.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/portals.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/ports.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/render.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/render.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/resources.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/restart.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/storage.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_6/validations.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/calibre-web/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/calibre-web/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/calibre-web/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/calibre-web/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/calibre/app.yaml b/ix-dev/community/calibre/app.yaml index 6e1b9e0526..4cb75b7a18 100644 --- a/ix-dev/community/calibre/app.yaml +++ b/ix-dev/community/calibre/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/calibre/icons/icon.png keywords: - media - ebooks -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -45,4 +45,4 @@ sources: - https://calibre-ebook.com/ title: Calibre train: community -version: 1.0.7 +version: 1.0.8 diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/validations.py b/ix-dev/community/calibre/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/calibre/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/configs.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/container.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/container.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/depends.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deps.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/device.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/device.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/devices.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/dns.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/environment.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/error.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/error.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/functions.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/labels.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/notes.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/portal.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/portals.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/ports.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/render.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/render.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/resources.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/restart.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/storage.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_6/validations.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/calibre/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/calibre/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/calibre/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/calibre/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/calibre/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/castopod/app.yaml b/ix-dev/community/castopod/app.yaml index ec72056fe5..48bb7d9ed1 100644 --- a/ix-dev/community/castopod/app.yaml +++ b/ix-dev/community/castopod/app.yaml @@ -19,8 +19,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/castopod/icons/icon.svg keywords: - podcast -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -49,4 +49,4 @@ sources: - https://code.castopod.org/adaures/castopod title: Castopod train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/validations.py b/ix-dev/community/castopod/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/castopod/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/configs.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/container.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/container.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/depends.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deps.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/device.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/device.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/devices.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/dns.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/environment.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/error.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/error.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/functions.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/labels.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/notes.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/portal.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/portals.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/ports.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/render.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/render.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/resources.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/restart.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/storage.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_6/validations.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/castopod/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/castopod/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/castopod/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/castopod/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/castopod/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/chia/app.yaml b/ix-dev/community/chia/app.yaml index f49d18888e..6060a66084 100644 --- a/ix-dev/community/chia/app.yaml +++ b/ix-dev/community/chia/app.yaml @@ -11,8 +11,8 @@ keywords: - blockchain - hard-drive - chia -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://www.chia.net/ title: Chia train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/validations.py b/ix-dev/community/chia/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/chia/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/chia/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/chia/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/chia/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/configs.py b/ix-dev/community/chia/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/container.py b/ix-dev/community/chia/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/container.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/depends.py b/ix-dev/community/chia/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deps.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/chia/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/device.py b/ix-dev/community/chia/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/device.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/devices.py b/ix-dev/community/chia/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/dns.py b/ix-dev/community/chia/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/environment.py b/ix-dev/community/chia/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/error.py b/ix-dev/community/chia/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/error.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/chia/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/functions.py b/ix-dev/community/chia/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/chia/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/labels.py b/ix-dev/community/chia/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/notes.py b/ix-dev/community/chia/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/portal.py b/ix-dev/community/chia/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/portals.py b/ix-dev/community/chia/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/ports.py b/ix-dev/community/chia/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/render.py b/ix-dev/community/chia/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/render.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/resources.py b/ix-dev/community/chia/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/restart.py b/ix-dev/community/chia/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/storage.py b/ix-dev/community/chia/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/chia/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_6/validations.py b/ix-dev/community/chia/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/chia/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/chia/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/chia/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/chia/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/chia/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/chia/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/chia/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/chia/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/chia/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/chia/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/clamav/app.yaml b/ix-dev/community/clamav/app.yaml index f69edca591..39584c09bc 100644 --- a/ix-dev/community/clamav/app.yaml +++ b/ix-dev/community/clamav/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/clamav/icons/icon.png keywords: - anti-virus - clamav -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://www.clamav.net/ title: ClamAV train: community -version: 1.2.10 +version: 1.2.11 diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/validations.py b/ix-dev/community/clamav/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/clamav/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/configs.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/container.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/container.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/depends.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deps.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/device.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/device.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/devices.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/dns.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/environment.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/error.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/error.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/functions.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/labels.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/notes.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/portal.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/portals.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/ports.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/render.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/render.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/resources.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/restart.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/storage.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_6/validations.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/clamav/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/clamav/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/clamav/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/clamav/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/clamav/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/cloudflared/app.yaml b/ix-dev/community/cloudflared/app.yaml index 88a614729a..d9bcaed8dd 100644 --- a/ix-dev/community/cloudflared/app.yaml +++ b/ix-dev/community/cloudflared/app.yaml @@ -11,8 +11,8 @@ keywords: - network - cloudflare - tunnel -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://hub.docker.com/r/cloudflare/cloudflared title: Cloudflared train: community -version: 1.2.6 +version: 1.2.7 diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/validations.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/configs.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/container.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/container.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/depends.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/device.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/device.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/devices.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/dns.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/environment.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/error.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/error.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/functions.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/labels.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/notes.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/portal.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/portals.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/ports.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/render.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/render.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/resources.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/restart.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/storage.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_6/validations.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/cloudflared/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/cloudflared/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/cloudflared/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/cloudflared/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/dashy/app.yaml b/ix-dev/community/dashy/app.yaml index 23c8644fe1..336fd63dbd 100644 --- a/ix-dev/community/dashy/app.yaml +++ b/ix-dev/community/dashy/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/dashy/icons/icon.png keywords: - dashboard -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://github.com/lissy93/dashy title: Dashy train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/validations.py b/ix-dev/community/dashy/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/dashy/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/configs.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/container.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/container.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/depends.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deps.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/device.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/device.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/devices.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/dns.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/environment.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/error.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/error.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/functions.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/labels.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/notes.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/portal.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/portals.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/ports.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/render.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/render.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/resources.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/restart.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/storage.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_6/validations.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/dashy/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/dashy/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/dashy/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/dashy/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/dashy/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/ddns-updater/app.yaml b/ix-dev/community/ddns-updater/app.yaml index 1c27ce6408..254e630ef4 100644 --- a/ix-dev/community/ddns-updater/app.yaml +++ b/ix-dev/community/ddns-updater/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/ddns-updater/icons/icon.svg keywords: - ddns-updater - ddns -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://hub.docker.com/r/qmcgaw/ddns-updater title: DDNS Updater train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/validations.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/configs.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/container.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/container.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/depends.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/device.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/device.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/devices.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/dns.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/environment.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/error.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/error.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/functions.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/labels.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/notes.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/portal.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/portals.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/ports.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/render.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/render.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/resources.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/restart.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/storage.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/validations.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/ddns-updater/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_3/__init__.py b/ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_4/__init__.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_3/__init__.py rename to ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_4/__init__.py diff --git a/ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_3/config.py b/ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_4/config.py similarity index 100% rename from ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_3/config.py rename to ix-dev/community/ddns-updater/templates/library/community/ddns-updater/v1_1_4/config.py diff --git a/ix-dev/community/deluge/app.yaml b/ix-dev/community/deluge/app.yaml index 7f757adcfc..9d0b7adc0f 100644 --- a/ix-dev/community/deluge/app.yaml +++ b/ix-dev/community/deluge/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/deluge/icons/icon.png keywords: - torrent - download -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://deluge-torrent.org/ title: Deluge train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/validations.py b/ix-dev/community/deluge/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/deluge/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/configs.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/container.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/container.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/depends.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deps.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/device.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/device.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/devices.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/dns.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/environment.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/error.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/error.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/functions.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/labels.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/notes.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/portal.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/portals.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/ports.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/render.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/render.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/resources.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/restart.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/storage.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_6/validations.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/deluge/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/deluge/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/deluge/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/deluge/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/deluge/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/distribution/app.yaml b/ix-dev/community/distribution/app.yaml index cbf59a2cac..f4656326ee 100644 --- a/ix-dev/community/distribution/app.yaml +++ b/ix-dev/community/distribution/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/distribution/icons/icon.svg keywords: - registry - container -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://github.com/distribution/distribution title: Distribution train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/validations.py b/ix-dev/community/distribution/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/distribution/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/configs.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/container.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/container.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/depends.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deps.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/device.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/device.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/devices.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/dns.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/environment.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/error.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/error.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/functions.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/labels.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/notes.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/portal.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/portals.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/ports.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/render.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/render.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/resources.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/restart.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/storage.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_6/validations.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/distribution/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/distribution/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/distribution/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/distribution/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/distribution/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/dockge/app.yaml b/ix-dev/community/dockge/app.yaml index 309caa4d8c..11363a2461 100644 --- a/ix-dev/community/dockge/app.yaml +++ b/ix-dev/community/dockge/app.yaml @@ -28,8 +28,8 @@ icon: https://media.sys.truenas.net/apps/dockge/icons/icon.svg keywords: - docker - compose -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -48,4 +48,4 @@ sources: - https://github.com/louislam/dockge title: Dockge train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/validations.py b/ix-dev/community/dockge/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/dockge/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/configs.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/container.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/container.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/depends.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deps.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/device.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/device.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/devices.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/dns.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/environment.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/error.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/error.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/functions.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/labels.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/notes.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/portal.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/portals.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/ports.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/render.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/render.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/resources.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/restart.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/storage.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_6/validations.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/dockge/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/dockge/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/dockge/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/dockge/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/dockge/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/drawio/app.yaml b/ix-dev/community/drawio/app.yaml index 9b351fa4f0..ce8f356a7f 100644 --- a/ix-dev/community/drawio/app.yaml +++ b/ix-dev/community/drawio/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/drawio/icons/icon.png keywords: - diagram - whiteboard -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/jgraph/drawio title: Draw.io train: community -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/validations.py b/ix-dev/community/drawio/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/drawio/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/configs.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/container.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/container.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/depends.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deps.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/device.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/device.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/devices.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/dns.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/environment.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/error.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/error.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/functions.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/labels.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/notes.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/portal.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/portals.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/ports.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/render.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/render.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/resources.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/restart.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/storage.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_6/validations.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/drawio/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/drawio/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/drawio/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/drawio/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/drawio/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/eclipse-mosquitto/app.yaml b/ix-dev/community/eclipse-mosquitto/app.yaml index 9d42e8dd3c..bc724350e8 100644 --- a/ix-dev/community/eclipse-mosquitto/app.yaml +++ b/ix-dev/community/eclipse-mosquitto/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/eclipse-mosquitto/icons/icon.svg keywords: - networking - mqtt -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://hub.docker.com/_/eclipse-mosquitto title: Eclipse Mosquitto train: community -version: 1.0.5 +version: 1.0.6 diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/validations.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/configs.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/container.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/container.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/depends.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/device.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/device.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/devices.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/dns.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/environment.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/error.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/error.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/functions.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/labels.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/notes.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/portal.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/portals.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/ports.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/render.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/render.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/resources.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/restart.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/storage.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/validations.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/eclipse-mosquitto/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/filebrowser/app.yaml b/ix-dev/community/filebrowser/app.yaml index 234f4dca8a..b488920903 100644 --- a/ix-dev/community/filebrowser/app.yaml +++ b/ix-dev/community/filebrowser/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/filebrowser/icons/icon.png keywords: - files - browser -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://hub.docker.com/r/filebrowser/filebrowser title: File Browser train: community -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/validations.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/configs.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/container.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/container.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/depends.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/device.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/device.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/devices.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/dns.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/environment.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/error.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/error.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/functions.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/labels.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/notes.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/portal.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/portals.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/ports.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/render.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/render.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/resources.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/restart.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/storage.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_6/validations.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/filebrowser/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/filebrowser/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/filebrowser/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/filebrowser/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/filestash/app.yaml b/ix-dev/community/filestash/app.yaml index 465aae3c4f..ac7ec82701 100644 --- a/ix-dev/community/filestash/app.yaml +++ b/ix-dev/community/filestash/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/filestash/icons/icon.svg keywords: - storage - file manager -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/mickael-kerjean/filestash title: Filestash train: community -version: 1.0.7 +version: 1.0.8 diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/validations.py b/ix-dev/community/filestash/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/filestash/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/configs.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/container.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/container.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/depends.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deps.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/device.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/device.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/devices.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/dns.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/environment.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/error.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/error.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/functions.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/labels.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/notes.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/portal.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/portals.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/ports.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/render.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/render.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/resources.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/restart.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/storage.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_6/validations.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/filestash/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/filestash/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/filestash/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/filestash/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/filestash/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/firefly-iii/app.yaml b/ix-dev/community/firefly-iii/app.yaml index 256d83e764..63a33dc758 100644 --- a/ix-dev/community/firefly-iii/app.yaml +++ b/ix-dev/community/firefly-iii/app.yaml @@ -19,8 +19,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/firefly-iii/icons/icon.png keywords: - finance -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -57,4 +57,4 @@ sources: - https://github.com/firefly-iii/firefly-iii title: Firefly III train: community -version: 1.4.4 +version: 1.4.5 diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/validations.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/configs.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/container.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/container.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/depends.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/device.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/device.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/devices.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/dns.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/environment.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/error.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/error.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/functions.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/labels.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/notes.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/portal.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/portals.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/ports.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/render.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/render.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/resources.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/restart.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/storage.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/validations.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/firefly-iii/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/firefly-iii/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/flame/app.yaml b/ix-dev/community/flame/app.yaml index d755d42b3a..f9e0f20528 100644 --- a/ix-dev/community/flame/app.yaml +++ b/ix-dev/community/flame/app.yaml @@ -14,8 +14,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/flame/icons/icon.png keywords: - startpage -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -34,4 +34,4 @@ sources: - https://github.com/pawelmalak/flame title: Flame train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/validations.py b/ix-dev/community/flame/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/flame/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/flame/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/flame/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/flame/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/configs.py b/ix-dev/community/flame/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/container.py b/ix-dev/community/flame/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/container.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/depends.py b/ix-dev/community/flame/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deps.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/flame/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/device.py b/ix-dev/community/flame/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/device.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/devices.py b/ix-dev/community/flame/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/dns.py b/ix-dev/community/flame/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/environment.py b/ix-dev/community/flame/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/error.py b/ix-dev/community/flame/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/error.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/flame/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/functions.py b/ix-dev/community/flame/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/flame/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/labels.py b/ix-dev/community/flame/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/notes.py b/ix-dev/community/flame/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/portal.py b/ix-dev/community/flame/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/portals.py b/ix-dev/community/flame/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/ports.py b/ix-dev/community/flame/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/render.py b/ix-dev/community/flame/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/render.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/resources.py b/ix-dev/community/flame/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/restart.py b/ix-dev/community/flame/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/storage.py b/ix-dev/community/flame/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/flame/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_6/validations.py b/ix-dev/community/flame/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/flame/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/flame/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/flame/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/flame/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/flame/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/flame/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/flame/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/flame/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/flame/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/flame/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/flaresolverr/app.yaml b/ix-dev/community/flaresolverr/app.yaml index 23b3dd6cae..f5ecea7f95 100644 --- a/ix-dev/community/flaresolverr/app.yaml +++ b/ix-dev/community/flaresolverr/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/flaresolverr/icons/icon.svg keywords: - networking - captcha -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -27,4 +27,4 @@ sources: - https://github.com/FlareSolverr/FlareSolverr title: FlareSolverr train: community -version: 1.0.12 +version: 1.0.13 diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/container.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/device.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/error.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/render.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/flaresolverr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/flaresolverr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/freshrss/app.yaml b/ix-dev/community/freshrss/app.yaml index 19bf97da59..bb39ded9e3 100644 --- a/ix-dev/community/freshrss/app.yaml +++ b/ix-dev/community/freshrss/app.yaml @@ -15,8 +15,8 @@ icon: https://media.sys.truenas.net/apps/freshrss/icons/icon.png keywords: - rss - news -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -41,4 +41,4 @@ sources: - https://hub.docker.com/r/freshrss/freshrss title: FreshRSS train: community -version: 1.3.2 +version: 1.3.3 diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/validations.py b/ix-dev/community/freshrss/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/freshrss/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/configs.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/container.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/container.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/depends.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deps.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/device.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/device.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/devices.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/dns.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/environment.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/error.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/error.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/functions.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/labels.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/notes.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/portal.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/portals.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/ports.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/render.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/render.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/resources.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/restart.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/storage.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_6/validations.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/freshrss/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/freshrss/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/freshrss/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/freshrss/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/freshrss/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/frigate/app.yaml b/ix-dev/community/frigate/app.yaml index 5cbe27936a..4cc3b329c3 100644 --- a/ix-dev/community/frigate/app.yaml +++ b/ix-dev/community/frigate/app.yaml @@ -21,8 +21,8 @@ icon: https://media.sys.truenas.net/apps/frigate/icons/icon.svg keywords: - camera - nvr -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://github.com/blakeblackshear/frigate title: Frigate train: community -version: 1.1.10 +version: 1.1.11 diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/validations.py b/ix-dev/community/frigate/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/frigate/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/configs.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/container.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/container.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/depends.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deps.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/device.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/device.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/devices.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/dns.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/environment.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/error.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/error.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/functions.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/labels.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/notes.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/portal.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/portals.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/ports.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/render.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/render.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/resources.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/restart.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/storage.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_6/validations.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/frigate/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/frigate/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/frigate/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/frigate/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/frigate/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/fscrawler/app.yaml b/ix-dev/community/fscrawler/app.yaml index bbe18fb7e8..604d42bafd 100644 --- a/ix-dev/community/fscrawler/app.yaml +++ b/ix-dev/community/fscrawler/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/fscrawler/icons/icon.svg keywords: - index - crawler -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://fscrawler.readthedocs.io/ title: FSCrawler train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/validations.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/configs.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/container.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/container.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/depends.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/device.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/device.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/devices.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/dns.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/environment.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/error.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/error.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/functions.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/labels.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/notes.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/portal.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/portals.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/ports.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/render.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/render.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/resources.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/restart.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/storage.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_6/validations.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/fscrawler/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/fscrawler/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/fscrawler/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/fscrawler/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/gaseous-server/app.yaml b/ix-dev/community/gaseous-server/app.yaml index e07340f4fc..721420c096 100644 --- a/ix-dev/community/gaseous-server/app.yaml +++ b/ix-dev/community/gaseous-server/app.yaml @@ -16,8 +16,8 @@ icon: https://media.sys.truenas.net/apps/gaseous-server/icons/icon.png keywords: - games - emulation -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -41,4 +41,4 @@ sources: - https://github.com/gaseous-project/gaseous-server title: Gaseous Server train: community -version: 1.0.9 +version: 1.0.10 diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/validations.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/configs.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/container.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/container.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/depends.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/device.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/device.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/devices.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/dns.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/environment.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/error.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/error.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/functions.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/labels.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/notes.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/portal.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/portals.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/ports.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/render.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/render.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/resources.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/restart.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/storage.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/validations.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/gaseous-server/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/gaseous-server/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/gitea/app.yaml b/ix-dev/community/gitea/app.yaml index e709d8db7b..efdea946c5 100644 --- a/ix-dev/community/gitea/app.yaml +++ b/ix-dev/community/gitea/app.yaml @@ -10,8 +10,8 @@ keywords: - git - gitea - source control -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://docs.gitea.io/en-us/install-with-docker-rootless title: Gitea train: community -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/validations.py b/ix-dev/community/gitea/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/gitea/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/configs.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/container.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/container.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/depends.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deps.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/device.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/device.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/devices.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/dns.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/environment.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/error.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/error.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/functions.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/labels.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/notes.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/portal.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/portals.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/ports.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/render.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/render.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/resources.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/restart.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/storage.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_6/validations.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/gitea/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/gitea/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/gitea/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/gitea/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/gitea/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/grafana/app.yaml b/ix-dev/community/grafana/app.yaml index 5ba7568aa2..5f38d42ce1 100644 --- a/ix-dev/community/grafana/app.yaml +++ b/ix-dev/community/grafana/app.yaml @@ -12,8 +12,8 @@ keywords: - monitoring - metrics - dashboards -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -34,4 +34,4 @@ sources: - https://github.com/grafana title: Grafana train: community -version: 1.2.5 +version: 1.2.6 diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/validations.py b/ix-dev/community/grafana/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/grafana/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/configs.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/container.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/container.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/depends.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deps.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/device.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/device.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/devices.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/dns.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/environment.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/error.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/error.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/functions.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/labels.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/notes.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/portal.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/portals.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/ports.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/render.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/render.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/resources.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/restart.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/storage.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_6/validations.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/grafana/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/grafana/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/grafana/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/grafana/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/grafana/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/handbrake/app.yaml b/ix-dev/community/handbrake/app.yaml index 68d22f4708..6c6f3c2752 100644 --- a/ix-dev/community/handbrake/app.yaml +++ b/ix-dev/community/handbrake/app.yaml @@ -28,8 +28,8 @@ keywords: - media - video - transcoder -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -47,4 +47,4 @@ sources: - https://hub.docker.com/r/jlesage/handbrake title: Handbrake train: community -version: 2.1.4 +version: 2.1.5 diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/validations.py b/ix-dev/community/handbrake/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/handbrake/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/configs.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/container.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/container.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/depends.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deps.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/device.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/device.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/devices.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/dns.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/environment.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/error.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/error.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/functions.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/labels.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/notes.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/portal.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/portals.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/ports.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/render.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/render.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/resources.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/restart.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/storage.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_6/validations.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/handbrake/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/handbrake/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/handbrake/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/handbrake/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/handbrake/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/homarr/app.yaml b/ix-dev/community/homarr/app.yaml index 348add43d7..52172964ce 100644 --- a/ix-dev/community/homarr/app.yaml +++ b/ix-dev/community/homarr/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/homarr/icons/icon.svg keywords: - dashboard -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/ajnart/homarr title: Homarr train: community -version: 1.1.6 +version: 1.1.7 diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/homarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/homarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/homarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/homarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/homarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/homarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/homarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/homepage/app.yaml b/ix-dev/community/homepage/app.yaml index a724ec65f3..03a2f4be75 100644 --- a/ix-dev/community/homepage/app.yaml +++ b/ix-dev/community/homepage/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/homepage/icons/icon.png keywords: - dashboard -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://github.com/benphelps/homepage title: Homepage train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/validations.py b/ix-dev/community/homepage/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/homepage/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/configs.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/container.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/container.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/depends.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deps.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/device.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/device.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/devices.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/dns.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/environment.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/error.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/error.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/functions.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/labels.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/notes.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/portal.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/portals.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/ports.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/render.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/render.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/resources.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/restart.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/storage.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_6/validations.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/homepage/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/homepage/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/homepage/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/homepage/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/homepage/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/homer/app.yaml b/ix-dev/community/homer/app.yaml index ebdecc4bbb..efda1adc9d 100644 --- a/ix-dev/community/homer/app.yaml +++ b/ix-dev/community/homer/app.yaml @@ -7,8 +7,8 @@ description: Homer is a dead simple static HOMepage for your servER to keep your home: https://github.com/bastienwirtz/homer host_mounts: [] icon: https://media.sys.truenas.net/apps/homer/icons/icon.png -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ tags: - homepage title: Homer train: community -version: 2.1.5 +version: 2.1.6 diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/validations.py b/ix-dev/community/homer/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/homer/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/homer/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/homer/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/homer/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/configs.py b/ix-dev/community/homer/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/container.py b/ix-dev/community/homer/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/container.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/depends.py b/ix-dev/community/homer/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deps.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/homer/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/device.py b/ix-dev/community/homer/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/device.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/devices.py b/ix-dev/community/homer/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/dns.py b/ix-dev/community/homer/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/environment.py b/ix-dev/community/homer/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/error.py b/ix-dev/community/homer/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/error.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/homer/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/functions.py b/ix-dev/community/homer/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/homer/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/labels.py b/ix-dev/community/homer/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/notes.py b/ix-dev/community/homer/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/portal.py b/ix-dev/community/homer/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/portals.py b/ix-dev/community/homer/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/ports.py b/ix-dev/community/homer/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/render.py b/ix-dev/community/homer/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/render.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/resources.py b/ix-dev/community/homer/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/restart.py b/ix-dev/community/homer/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/storage.py b/ix-dev/community/homer/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/homer/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_6/validations.py b/ix-dev/community/homer/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/homer/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/homer/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/homer/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/homer/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/homer/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/homer/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/homer/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/homer/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/homer/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/homer/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/iconik-storage-gateway/app.yaml b/ix-dev/community/iconik-storage-gateway/app.yaml index 6b5af27f2b..6324671275 100644 --- a/ix-dev/community/iconik-storage-gateway/app.yaml +++ b/ix-dev/community/iconik-storage-gateway/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/iconik-storage-gateway/icons/icon.svg keywords: - iconik -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://app.iconik.io/help/pages/isg title: Iconik Storage Gateway train: community -version: 1.0.5 +version: 1.0.6 diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/validations.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/configs.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/container.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/container.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/depends.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/device.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/device.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/devices.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/dns.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/environment.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/error.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/error.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/functions.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/labels.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/notes.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/portal.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/portals.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/ports.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/render.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/render.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/resources.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/restart.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/storage.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/validations.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/iconik-storage-gateway/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/immich/app.yaml b/ix-dev/community/immich/app.yaml index a131f4aa52..598979b90b 100644 --- a/ix-dev/community/immich/app.yaml +++ b/ix-dev/community/immich/app.yaml @@ -16,8 +16,8 @@ icon: https://media.sys.truenas.net/apps/immich/icons/icon.svg keywords: - photos - backup -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -45,4 +45,4 @@ sources: - https://github.com/immich-app/immich title: Immich train: community -version: 1.7.14 +version: 1.7.15 diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/validations.py b/ix-dev/community/immich/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/immich/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/immich/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/immich/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/immich/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/configs.py b/ix-dev/community/immich/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/container.py b/ix-dev/community/immich/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/container.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/depends.py b/ix-dev/community/immich/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deps.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/immich/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/device.py b/ix-dev/community/immich/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/device.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/devices.py b/ix-dev/community/immich/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/dns.py b/ix-dev/community/immich/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/environment.py b/ix-dev/community/immich/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/error.py b/ix-dev/community/immich/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/error.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/immich/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/functions.py b/ix-dev/community/immich/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/immich/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/labels.py b/ix-dev/community/immich/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/notes.py b/ix-dev/community/immich/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/portal.py b/ix-dev/community/immich/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/portals.py b/ix-dev/community/immich/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/ports.py b/ix-dev/community/immich/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/render.py b/ix-dev/community/immich/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/render.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/resources.py b/ix-dev/community/immich/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/restart.py b/ix-dev/community/immich/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/storage.py b/ix-dev/community/immich/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/immich/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_6/validations.py b/ix-dev/community/immich/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/immich/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/immich/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/immich/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/immich/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/immich/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/immich/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/immich/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/immich/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/immich/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/immich/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/invidious/app.yaml b/ix-dev/community/invidious/app.yaml index 4e5ed5f4ba..b9e38c7723 100644 --- a/ix-dev/community/invidious/app.yaml +++ b/ix-dev/community/invidious/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/invidious/icons/icon.svg keywords: - youtube -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://quay.io/repository/invidious title: Invidious train: community -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/validations.py b/ix-dev/community/invidious/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/invidious/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/configs.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/container.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/container.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/depends.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deps.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/device.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/device.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/devices.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/dns.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/environment.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/error.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/error.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/functions.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/labels.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/notes.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/portal.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/portals.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/ports.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/render.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/render.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/resources.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/restart.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/storage.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_6/validations.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/invidious/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/invidious/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/invidious/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/invidious/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/invidious/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/ipfs/app.yaml b/ix-dev/community/ipfs/app.yaml index 84c89d9b98..2a27b6bcfd 100644 --- a/ix-dev/community/ipfs/app.yaml +++ b/ix-dev/community/ipfs/app.yaml @@ -12,8 +12,8 @@ keywords: - ipfs - file-sharing - kubo -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://ipfs.tech/ title: IPFS train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/validations.py b/ix-dev/community/ipfs/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/ipfs/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/configs.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/container.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/container.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/depends.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deps.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/device.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/device.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/devices.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/dns.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/environment.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/error.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/error.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/functions.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/labels.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/notes.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/portal.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/portals.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/ports.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/render.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/render.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/resources.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/restart.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/storage.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_6/validations.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/ipfs/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/ipfs/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/ipfs/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/ipfs/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/ipfs/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/jellyfin/app.yaml b/ix-dev/community/jellyfin/app.yaml index 1ce050cab2..8a4cf9c139 100644 --- a/ix-dev/community/jellyfin/app.yaml +++ b/ix-dev/community/jellyfin/app.yaml @@ -14,8 +14,8 @@ keywords: - tv - media - streaming -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -35,4 +35,4 @@ sources: - https://jellyfin.org/ title: Jellyfin train: community -version: 1.1.10 +version: 1.1.11 diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/validations.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/configs.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/container.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/container.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/depends.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/device.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/device.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/devices.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/dns.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/environment.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/error.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/error.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/functions.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/labels.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/notes.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/portal.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/portals.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/ports.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/render.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/render.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/resources.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/restart.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/storage.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_6/validations.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/jellyfin/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/jellyfin/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/jellyfin/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/jellyfin/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/jellyseerr/app.yaml b/ix-dev/community/jellyseerr/app.yaml index 9012050a95..672bea78d5 100644 --- a/ix-dev/community/jellyseerr/app.yaml +++ b/ix-dev/community/jellyseerr/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/jellyseerr/icons/icon.svg keywords: - media -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://hub.docker.com/r/fallenbagel/jellyseerr title: Jellyseerr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/container.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/device.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/error.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/render.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/jellyseerr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/jellyseerr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/jenkins/app.yaml b/ix-dev/community/jenkins/app.yaml index ee27cc77da..713c4262bb 100644 --- a/ix-dev/community/jenkins/app.yaml +++ b/ix-dev/community/jenkins/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/jenkins/icons/icon.svg keywords: - automation - ci/cd -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://www.jenkins.io/ title: Jenkins train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/validations.py b/ix-dev/community/jenkins/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/jenkins/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/configs.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/container.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/container.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/depends.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deps.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/device.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/device.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/devices.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/dns.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/environment.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/error.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/error.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/functions.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/labels.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/notes.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/portal.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/portals.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/ports.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/render.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/render.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/resources.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/restart.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/storage.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_6/validations.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/jenkins/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/jenkins/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/jenkins/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/jenkins/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/jenkins/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/joplin/app.yaml b/ix-dev/community/joplin/app.yaml index 067d464024..f4a1e9bc66 100644 --- a/ix-dev/community/joplin/app.yaml +++ b/ix-dev/community/joplin/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/joplin/icons/icon.png keywords: - notes -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -36,4 +36,4 @@ sources: - https://hub.docker.com/r/joplin/server/ title: Joplin train: community -version: 1.3.2 +version: 1.3.3 diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/validations.py b/ix-dev/community/joplin/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/joplin/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/configs.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/container.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/container.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/depends.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deps.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/device.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/device.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/devices.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/dns.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/environment.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/error.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/error.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/functions.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/labels.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/notes.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/portal.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/portals.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/ports.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/render.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/render.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/resources.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/restart.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/storage.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_6/validations.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/joplin/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/joplin/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/joplin/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/joplin/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/joplin/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/kapowarr/app.yaml b/ix-dev/community/kapowarr/app.yaml index 86ba4c7156..6b5cf95b5f 100644 --- a/ix-dev/community/kapowarr/app.yaml +++ b/ix-dev/community/kapowarr/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/kapowarr/icons/icon.svg keywords: - comic - media -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/Casvt/Kapowarr title: Kapowarr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/kapowarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/kapowarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/kapowarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/kapowarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/kavita/app.yaml b/ix-dev/community/kavita/app.yaml index 289f56c6b2..1a9b66b6d2 100644 --- a/ix-dev/community/kavita/app.yaml +++ b/ix-dev/community/kavita/app.yaml @@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/kavita/icons/icon.png keywords: - ebook - manga -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -44,4 +44,4 @@ sources: - https://www.kavitareader.com title: Kavita train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/validations.py b/ix-dev/community/kavita/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/kavita/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/configs.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/container.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/container.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/depends.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deps.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/device.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/device.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/devices.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/dns.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/environment.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/error.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/error.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/functions.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/labels.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/notes.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/portal.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/portals.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/ports.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/render.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/render.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/resources.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/restart.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/storage.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_6/validations.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/kavita/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/kavita/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/kavita/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/kavita/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/kavita/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/komga/app.yaml b/ix-dev/community/komga/app.yaml index 00de8999a7..df0863a291 100644 --- a/ix-dev/community/komga/app.yaml +++ b/ix-dev/community/komga/app.yaml @@ -10,8 +10,8 @@ keywords: - media - comics - mangas -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://hub.docker.com/r/gotson/komga title: Komga train: community -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/validations.py b/ix-dev/community/komga/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/komga/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/komga/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/komga/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/komga/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/configs.py b/ix-dev/community/komga/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/container.py b/ix-dev/community/komga/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/container.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/depends.py b/ix-dev/community/komga/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deps.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/komga/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/device.py b/ix-dev/community/komga/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/device.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/devices.py b/ix-dev/community/komga/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/dns.py b/ix-dev/community/komga/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/environment.py b/ix-dev/community/komga/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/error.py b/ix-dev/community/komga/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/error.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/komga/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/functions.py b/ix-dev/community/komga/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/komga/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/labels.py b/ix-dev/community/komga/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/notes.py b/ix-dev/community/komga/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/portal.py b/ix-dev/community/komga/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/portals.py b/ix-dev/community/komga/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/ports.py b/ix-dev/community/komga/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/render.py b/ix-dev/community/komga/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/render.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/resources.py b/ix-dev/community/komga/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/restart.py b/ix-dev/community/komga/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/storage.py b/ix-dev/community/komga/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/komga/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_6/validations.py b/ix-dev/community/komga/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/komga/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/komga/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/komga/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/komga/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/komga/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/komga/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/komga/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/komga/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/komga/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/komga/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/lidarr/app.yaml b/ix-dev/community/lidarr/app.yaml index a2196d8d93..53634c5c22 100644 --- a/ix-dev/community/lidarr/app.yaml +++ b/ix-dev/community/lidarr/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/lidarr/icons/icon.png keywords: - media - music -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/Lidarr/Lidarr title: Lidarr train: community -version: 1.2.7 +version: 1.2.8 diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/lidarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/lidarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/lidarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/lidarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/lidarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/lidarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/lidarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/linkding/app.yaml b/ix-dev/community/linkding/app.yaml index 2c788613fc..849b93849a 100644 --- a/ix-dev/community/linkding/app.yaml +++ b/ix-dev/community/linkding/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/linkding/icons/icon.svg keywords: - bookmark -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://hub.docker.com/r/sissbruecker/linkding/ title: Linkding train: community -version: 1.2.2 +version: 1.2.3 diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/validations.py b/ix-dev/community/linkding/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/linkding/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/configs.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/container.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/container.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/depends.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deps.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/device.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/device.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/devices.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/dns.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/environment.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/error.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/error.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/functions.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/labels.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/notes.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/portal.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/portals.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/ports.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/render.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/render.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/resources.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/restart.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/storage.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_6/validations.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/linkding/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/linkding/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/linkding/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/linkding/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/linkding/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/listmonk/app.yaml b/ix-dev/community/listmonk/app.yaml index f96a0d3f4d..e373a8fa9a 100644 --- a/ix-dev/community/listmonk/app.yaml +++ b/ix-dev/community/listmonk/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/listmonk/icons/icon.svg keywords: - mailing-list - newsletter -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -46,4 +46,4 @@ sources: - https://github.com/knadh/listmonk title: Listmonk train: community -version: 1.2.2 +version: 1.2.3 diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/validations.py b/ix-dev/community/listmonk/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/listmonk/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/configs.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/container.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/container.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/depends.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deps.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/device.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/device.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/devices.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/dns.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/environment.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/error.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/error.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/functions.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/labels.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/notes.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/portal.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/portals.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/ports.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/render.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/render.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/resources.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/restart.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/storage.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_6/validations.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/listmonk/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/listmonk/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/listmonk/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/listmonk/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/listmonk/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/logseq/app.yaml b/ix-dev/community/logseq/app.yaml index 749eb68dc6..fabf292670 100644 --- a/ix-dev/community/logseq/app.yaml +++ b/ix-dev/community/logseq/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/logseq/icons/icon.png keywords: - knowledge - management -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://github.com/logseq/logseq title: Logseq train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/validations.py b/ix-dev/community/logseq/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/logseq/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/configs.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/container.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/container.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/depends.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deps.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/device.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/device.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/devices.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/dns.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/environment.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/error.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/error.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/functions.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/labels.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/notes.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/portal.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/portals.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/ports.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/render.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/render.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/resources.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/restart.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/storage.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_6/validations.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/logseq/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/logseq/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/logseq/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/logseq/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/logseq/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/mealie/app.yaml b/ix-dev/community/mealie/app.yaml index f21844a73b..7811a2b439 100644 --- a/ix-dev/community/mealie/app.yaml +++ b/ix-dev/community/mealie/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/mealie/icons/icon.png keywords: - recipes - meal planner -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://docs.mealie.io/ title: Mealie train: community -version: 1.4.4 +version: 1.4.5 diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/validations.py b/ix-dev/community/mealie/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/mealie/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/configs.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/container.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/container.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/depends.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deps.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/device.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/device.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/devices.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/dns.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/environment.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/error.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/error.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/functions.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/labels.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/notes.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/portal.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/portals.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/ports.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/render.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/render.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/resources.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/restart.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/storage.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_6/validations.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/mealie/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/mealie/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/mealie/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/mealie/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/mealie/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/metube/app.yaml b/ix-dev/community/metube/app.yaml index 362afb7718..27c9308235 100644 --- a/ix-dev/community/metube/app.yaml +++ b/ix-dev/community/metube/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/metube/icons/icon.svg keywords: - youtube-dl - yt-dlp -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://github.com/alexta69/metube title: MeTube train: community -version: 1.2.7 +version: 1.2.8 diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/validations.py b/ix-dev/community/metube/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/metube/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/metube/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/metube/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/metube/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/configs.py b/ix-dev/community/metube/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/container.py b/ix-dev/community/metube/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/container.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/depends.py b/ix-dev/community/metube/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deps.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/metube/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/device.py b/ix-dev/community/metube/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/device.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/devices.py b/ix-dev/community/metube/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/dns.py b/ix-dev/community/metube/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/environment.py b/ix-dev/community/metube/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/error.py b/ix-dev/community/metube/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/error.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/metube/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/functions.py b/ix-dev/community/metube/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/metube/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/labels.py b/ix-dev/community/metube/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/notes.py b/ix-dev/community/metube/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/portal.py b/ix-dev/community/metube/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/portals.py b/ix-dev/community/metube/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/ports.py b/ix-dev/community/metube/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/render.py b/ix-dev/community/metube/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/render.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/resources.py b/ix-dev/community/metube/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/restart.py b/ix-dev/community/metube/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/storage.py b/ix-dev/community/metube/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/metube/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_6/validations.py b/ix-dev/community/metube/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/metube/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/metube/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/metube/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/metube/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/metube/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/metube/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/metube/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/metube/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/metube/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/metube/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/minecraft/app.yaml b/ix-dev/community/minecraft/app.yaml index 93c002825d..428f3d4a54 100644 --- a/ix-dev/community/minecraft/app.yaml +++ b/ix-dev/community/minecraft/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/minecraft/icons/icon.svg keywords: - world - building -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://github.com/itzg/docker-minecraft-server title: Minecraft train: community -version: 1.12.5 +version: 1.12.6 diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/validations.py b/ix-dev/community/minecraft/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/minecraft/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/configs.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/container.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/container.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/depends.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deps.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/device.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/device.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/devices.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/dns.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/environment.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/error.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/error.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/functions.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/labels.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/notes.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/portal.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/portals.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/ports.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/render.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/render.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/resources.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/restart.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/storage.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_6/validations.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/minecraft/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/minecraft/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/minecraft/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/minecraft/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/minecraft/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/mineos/app.yaml b/ix-dev/community/mineos/app.yaml index 5cc18a21e3..304cba2b93 100644 --- a/ix-dev/community/mineos/app.yaml +++ b/ix-dev/community/mineos/app.yaml @@ -19,8 +19,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/mineos/icons/icon.png keywords: - minecraft -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -39,4 +39,4 @@ sources: - https://github.com/hexparrot/mineos-node title: MineOS train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/validations.py b/ix-dev/community/mineos/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/mineos/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/configs.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/container.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/container.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/depends.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deps.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/device.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/device.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/devices.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/dns.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/environment.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/error.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/error.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/functions.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/labels.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/notes.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/portal.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/portals.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/ports.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/render.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/render.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/resources.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/restart.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/storage.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_6/validations.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/mineos/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/mineos/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/mineos/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/mineos/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/mineos/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/mumble/app.yaml b/ix-dev/community/mumble/app.yaml index e67d89e25f..1ac1c6ec4f 100644 --- a/ix-dev/community/mumble/app.yaml +++ b/ix-dev/community/mumble/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/mumble/icons/icon.svg keywords: - voice -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -27,4 +27,4 @@ sources: - https://www.mumble.info/ title: Mumble train: community -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/validations.py b/ix-dev/community/mumble/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/mumble/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/configs.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/container.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/container.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/depends.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deps.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/device.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/device.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/devices.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/dns.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/environment.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/error.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/error.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/functions.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/labels.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/notes.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/portal.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/portals.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/ports.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/render.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/render.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/resources.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/restart.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/storage.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_6/validations.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/mumble/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/mumble/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/mumble/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/mumble/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/mumble/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/n8n/app.yaml b/ix-dev/community/n8n/app.yaml index 38abb4f464..b4b81e0a95 100644 --- a/ix-dev/community/n8n/app.yaml +++ b/ix-dev/community/n8n/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/n8n/icons/icon.png keywords: - workflows - automation -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://hub.docker.com/r/n8nio/n8n title: n8n train: community -version: 1.5.7 +version: 1.5.8 diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/validations.py b/ix-dev/community/n8n/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/n8n/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/configs.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/container.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/container.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/depends.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deps.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/device.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/device.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/devices.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/dns.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/environment.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/error.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/error.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/functions.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/labels.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/notes.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/portal.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/portals.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/ports.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/render.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/render.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/resources.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/restart.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/storage.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_6/validations.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/n8n/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/n8n/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/n8n/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/n8n/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/n8n/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/navidrome/app.yaml b/ix-dev/community/navidrome/app.yaml index ab0ed5cf98..c0912f83b8 100644 --- a/ix-dev/community/navidrome/app.yaml +++ b/ix-dev/community/navidrome/app.yaml @@ -11,8 +11,8 @@ icon: https://media.sys.truenas.net/apps/navidrome/icons/icon.png keywords: - media - music -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/navidrome/navidrome/ title: Navidrome train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/validations.py b/ix-dev/community/navidrome/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/navidrome/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/configs.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/container.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/container.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/depends.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deps.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/device.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/device.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/devices.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/dns.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/environment.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/error.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/error.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/functions.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/labels.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/notes.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/portal.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/portals.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/ports.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/render.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/render.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/resources.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/restart.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/storage.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_6/validations.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/navidrome/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/navidrome/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/navidrome/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/navidrome/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/navidrome/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/netbootxyz/app.yaml b/ix-dev/community/netbootxyz/app.yaml index e256e9e01f..1e0f0d64d5 100644 --- a/ix-dev/community/netbootxyz/app.yaml +++ b/ix-dev/community/netbootxyz/app.yaml @@ -30,8 +30,8 @@ keywords: - netboot - netbootxyz - netboot.xyz -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -50,4 +50,4 @@ sources: - https://netboot.xyz title: Netboot.xyz train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/validations.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/configs.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/container.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/container.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/depends.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/device.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/device.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/devices.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/dns.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/environment.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/error.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/error.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/functions.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/labels.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/notes.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/portal.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/portals.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/ports.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/render.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/render.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/resources.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/restart.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/storage.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/validations.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/netbootxyz/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/netbootxyz/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/nginx-proxy-manager/app.yaml b/ix-dev/community/nginx-proxy-manager/app.yaml index 920e778d01..8fcfe45342 100644 --- a/ix-dev/community/nginx-proxy-manager/app.yaml +++ b/ix-dev/community/nginx-proxy-manager/app.yaml @@ -22,8 +22,8 @@ keywords: - reverse - nginx - proxy -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -44,4 +44,4 @@ sources: - https://hub.docker.com/r/jc21/nginx-proxy-manager title: Nginx Proxy Manager train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/validations.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/configs.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/container.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/container.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/depends.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/device.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/device.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/devices.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/dns.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/environment.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/error.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/error.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/functions.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/labels.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/notes.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/portal.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/portals.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/ports.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/render.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/render.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/resources.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/restart.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/storage.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/validations.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/nginx-proxy-manager/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/node-red/app.yaml b/ix-dev/community/node-red/app.yaml index 3ec06cbf15..71e86f01e1 100644 --- a/ix-dev/community/node-red/app.yaml +++ b/ix-dev/community/node-red/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/node-red/icons/icon.png keywords: - automation -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://github.com/node-red/node-red-docker title: Node-RED train: community -version: 1.1.6 +version: 1.1.7 diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/validations.py b/ix-dev/community/node-red/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/node-red/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/configs.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/container.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/container.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/depends.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deps.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/device.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/device.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/devices.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/dns.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/environment.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/error.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/error.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/functions.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/labels.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/notes.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/portal.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/portals.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/ports.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/render.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/render.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/resources.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/restart.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/storage.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_6/validations.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/node-red/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/node-red/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/node-red/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/node-red/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/node-red/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/odoo/app.yaml b/ix-dev/community/odoo/app.yaml index 47e2ce037f..f5ebe32b31 100644 --- a/ix-dev/community/odoo/app.yaml +++ b/ix-dev/community/odoo/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/odoo/icons/icon.png keywords: - erp - odoo -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -35,4 +35,4 @@ sources: - https://github.com/odoo/odoo title: Odoo train: community -version: 1.2.2 +version: 1.2.3 diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/validations.py b/ix-dev/community/odoo/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/odoo/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/configs.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/container.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/container.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/depends.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deps.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/device.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/device.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/devices.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/dns.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/environment.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/error.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/error.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/functions.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/labels.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/notes.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/portal.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/portals.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/ports.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/render.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/render.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/resources.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/restart.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/storage.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_6/validations.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/odoo/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/odoo/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/odoo/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/odoo/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/odoo/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/ollama/app.yaml b/ix-dev/community/ollama/app.yaml index a67bd7353a..9dd2343051 100644 --- a/ix-dev/community/ollama/app.yaml +++ b/ix-dev/community/ollama/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/ollama/icons/icon.png keywords: - ai - llm -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://github.com/ollama/ollama title: Ollama train: community -version: 1.0.22 +version: 1.0.23 diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/validations.py b/ix-dev/community/ollama/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/ollama/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/configs.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/container.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/container.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/depends.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deps.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/device.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/device.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/devices.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/dns.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/environment.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/error.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/error.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/functions.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/labels.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/notes.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/portal.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/portals.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/ports.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/render.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/render.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/resources.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/restart.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/storage.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_6/validations.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/ollama/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/ollama/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/ollama/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/ollama/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/ollama/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/omada-controller/app.yaml b/ix-dev/community/omada-controller/app.yaml index 5cb04d9e60..cbc3a0efc0 100644 --- a/ix-dev/community/omada-controller/app.yaml +++ b/ix-dev/community/omada-controller/app.yaml @@ -23,8 +23,8 @@ keywords: - controller - omada - tp-link -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://hub.docker.com/r/mbentley/omada-controller title: Omada Controller train: community -version: 1.2.6 +version: 1.2.7 diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/validations.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/configs.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/container.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/container.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/depends.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/device.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/device.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/devices.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/dns.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/environment.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/error.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/error.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/functions.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/labels.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/notes.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/portal.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/portals.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/ports.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/render.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/render.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/resources.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/restart.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/storage.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_6/validations.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/omada-controller/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/omada-controller/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/omada-controller/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/omada-controller/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/open-webui/app.yaml b/ix-dev/community/open-webui/app.yaml index e1c4c68e2f..5742261e4c 100644 --- a/ix-dev/community/open-webui/app.yaml +++ b/ix-dev/community/open-webui/app.yaml @@ -13,8 +13,8 @@ keywords: - llm - webui - open-webui -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/open-webui/open-webui title: Open WebUI train: community -version: 1.0.17 +version: 1.0.18 diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/validations.py b/ix-dev/community/open-webui/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/open-webui/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/configs.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/container.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/container.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/depends.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deps.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/device.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/device.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/devices.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/dns.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/environment.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/error.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/error.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/functions.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/labels.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/notes.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/portal.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/portals.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/ports.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/render.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/render.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/resources.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/restart.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/storage.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_6/validations.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/open-webui/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/open-webui/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/open-webui/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/open-webui/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/open-webui/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/organizr/app.yaml b/ix-dev/community/organizr/app.yaml index 510434a344..4137ffdbb2 100644 --- a/ix-dev/community/organizr/app.yaml +++ b/ix-dev/community/organizr/app.yaml @@ -19,8 +19,8 @@ icon: https://media.sys.truenas.net/apps/organizr/icons/icon.png keywords: - dashboard - organizr -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -40,4 +40,4 @@ sources: - https://github.com/causefx/Organizr title: Organizr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/organizr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/organizr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/container.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/device.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/error.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/render.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/organizr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/organizr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/organizr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/organizr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/organizr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/overseerr/app.yaml b/ix-dev/community/overseerr/app.yaml index df467a4f9b..57cf8073bb 100644 --- a/ix-dev/community/overseerr/app.yaml +++ b/ix-dev/community/overseerr/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/overseerr/icons/icon.svg keywords: - media -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://github.com/sct/overseerr title: Overseerr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/overseerr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/overseerr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/container.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/device.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/error.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/render.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/overseerr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/overseerr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/overseerr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/overseerr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/overseerr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/palworld/app.yaml b/ix-dev/community/palworld/app.yaml index 14d246b33e..edcb1f190c 100644 --- a/ix-dev/community/palworld/app.yaml +++ b/ix-dev/community/palworld/app.yaml @@ -28,8 +28,8 @@ icon: https://media.sys.truenas.net/apps/palworld/icons/icon.webp keywords: - game - palworld -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -47,4 +47,4 @@ sources: - https://github.com/ich777/docker-steamcmd-server/tree/palworld title: Palworld train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/validations.py b/ix-dev/community/palworld/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/palworld/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/configs.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/container.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/container.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/depends.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deps.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/device.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/device.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/devices.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/dns.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/environment.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/error.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/error.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/functions.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/labels.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/notes.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/portal.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/portals.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/ports.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/render.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/render.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/resources.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/restart.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/storage.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_6/validations.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/palworld/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/palworld/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/palworld/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/palworld/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/palworld/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/paperless-ngx/app.yaml b/ix-dev/community/paperless-ngx/app.yaml index 512f76eb34..ee3f5205d0 100644 --- a/ix-dev/community/paperless-ngx/app.yaml +++ b/ix-dev/community/paperless-ngx/app.yaml @@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/paperless-ngx/icons/icon.svg keywords: - document - management -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -67,4 +67,4 @@ sources: - https://github.com/paperless-ngx/paperless-ngx title: Paperless-ngx train: community -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/validations.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/configs.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/container.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/container.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/depends.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/device.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/device.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/devices.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/dns.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/environment.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/error.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/error.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/functions.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/labels.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/notes.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/portal.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/portals.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/ports.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/render.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/render.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/resources.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/restart.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/storage.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/validations.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/paperless-ngx/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/paperless-ngx/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/passbolt/app.yaml b/ix-dev/community/passbolt/app.yaml index f4c16a989d..bf2ae86b8a 100644 --- a/ix-dev/community/passbolt/app.yaml +++ b/ix-dev/community/passbolt/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/passbolt/icons/icon.svg keywords: - password - manager -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -37,4 +37,4 @@ sources: - https://www.passbolt.com title: Passbolt train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/validations.py b/ix-dev/community/passbolt/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/passbolt/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/configs.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/container.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/container.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/depends.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deps.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/device.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/device.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/devices.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/dns.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/environment.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/error.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/error.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/functions.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/labels.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/notes.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/portal.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/portals.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/ports.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/render.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/render.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/resources.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/restart.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/storage.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_6/validations.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/passbolt/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/passbolt/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/passbolt/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/passbolt/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/passbolt/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/penpot/app.yaml b/ix-dev/community/penpot/app.yaml index 73e855347e..38170f7167 100644 --- a/ix-dev/community/penpot/app.yaml +++ b/ix-dev/community/penpot/app.yaml @@ -18,8 +18,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/penpot/icons/icon.svg keywords: - design -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -52,4 +52,4 @@ sources: - https://github.com/penpot/penpot title: Penpot train: community -version: 1.1.2 +version: 1.1.3 diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/validations.py b/ix-dev/community/penpot/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/penpot/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/configs.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/container.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/container.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/depends.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deps.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/device.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/device.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/devices.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/dns.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/environment.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/error.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/error.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/functions.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/labels.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/notes.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/portal.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/portals.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/ports.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/render.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/render.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/resources.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/restart.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/storage.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_6/validations.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/penpot/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/penpot/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/penpot/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/penpot/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/penpot/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/pgadmin/app.yaml b/ix-dev/community/pgadmin/app.yaml index d814b55676..31a05bc64b 100644 --- a/ix-dev/community/pgadmin/app.yaml +++ b/ix-dev/community/pgadmin/app.yaml @@ -12,8 +12,8 @@ icon: https://media.sys.truenas.net/apps/pgadmin/icons/icon.png keywords: - database - management -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -34,4 +34,4 @@ sources: - https://www.pgadmin.org/ title: pgAdmin train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/validations.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/configs.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/container.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/container.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/depends.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/device.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/device.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/devices.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/dns.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/environment.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/error.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/error.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/functions.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/labels.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/notes.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/portal.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/portals.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/ports.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/render.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/render.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/resources.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/restart.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/storage.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_6/validations.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/pgadmin/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/pgadmin/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/pgadmin/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/pgadmin/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/pigallery2/app.yaml b/ix-dev/community/pigallery2/app.yaml index 173eaf9031..49934ddf41 100644 --- a/ix-dev/community/pigallery2/app.yaml +++ b/ix-dev/community/pigallery2/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/pigallery2/icons/icon.png keywords: - photo - media -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -39,4 +39,4 @@ sources: - https://github.com/bpatrik/pigallery2 title: PiGallery2 train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/validations.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/configs.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/container.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/container.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/depends.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/device.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/device.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/devices.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/dns.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/environment.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/error.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/error.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/functions.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/labels.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/notes.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/portal.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/portals.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/ports.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/render.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/render.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/resources.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/restart.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/storage.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_6/validations.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/pigallery2/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/pigallery2/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/pigallery2/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/pigallery2/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/piwigo/app.yaml b/ix-dev/community/piwigo/app.yaml index 1fe8ff8335..726da76f39 100644 --- a/ix-dev/community/piwigo/app.yaml +++ b/ix-dev/community/piwigo/app.yaml @@ -22,8 +22,8 @@ icon: https://media.sys.truenas.net/apps/piwigo/icons/icon.svg keywords: - photo - gallery -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -47,4 +47,4 @@ sources: - https://hub.docker.com/r/linuxserver/piwigo title: Piwigo train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/validations.py b/ix-dev/community/piwigo/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/piwigo/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/configs.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/container.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/container.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/depends.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deps.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/device.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/device.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/devices.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/dns.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/environment.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/error.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/error.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/functions.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/labels.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/notes.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/portal.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/portals.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/ports.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/render.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/render.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/resources.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/restart.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/storage.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_6/validations.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/piwigo/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/piwigo/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/piwigo/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/piwigo/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/piwigo/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/planka/app.yaml b/ix-dev/community/planka/app.yaml index ce32003324..1078c1e66b 100644 --- a/ix-dev/community/planka/app.yaml +++ b/ix-dev/community/planka/app.yaml @@ -10,8 +10,8 @@ keywords: - kanban - project - task -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -35,4 +35,4 @@ sources: - https://github.com/plankanban/planka title: Planka train: community -version: 1.2.2 +version: 1.2.3 diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/validations.py b/ix-dev/community/planka/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/planka/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/planka/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/planka/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/planka/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/configs.py b/ix-dev/community/planka/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/container.py b/ix-dev/community/planka/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/container.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/depends.py b/ix-dev/community/planka/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deps.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/planka/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/device.py b/ix-dev/community/planka/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/device.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/devices.py b/ix-dev/community/planka/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/dns.py b/ix-dev/community/planka/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/environment.py b/ix-dev/community/planka/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/error.py b/ix-dev/community/planka/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/error.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/planka/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/functions.py b/ix-dev/community/planka/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/planka/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/labels.py b/ix-dev/community/planka/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/notes.py b/ix-dev/community/planka/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/portal.py b/ix-dev/community/planka/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/portals.py b/ix-dev/community/planka/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/ports.py b/ix-dev/community/planka/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/render.py b/ix-dev/community/planka/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/render.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/resources.py b/ix-dev/community/planka/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/restart.py b/ix-dev/community/planka/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/storage.py b/ix-dev/community/planka/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/planka/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_6/validations.py b/ix-dev/community/planka/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/planka/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/planka/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/planka/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/planka/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/planka/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/planka/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/planka/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/planka/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/planka/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/planka/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/plex-auto-languages/app.yaml b/ix-dev/community/plex-auto-languages/app.yaml index 9cbb110669..b5c93b2630 100644 --- a/ix-dev/community/plex-auto-languages/app.yaml +++ b/ix-dev/community/plex-auto-languages/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/plex-auto-languages/icons/icon.svg keywords: - plex - languages -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -27,4 +27,4 @@ sources: - https://github.com/JourneyDocker/Plex-Auto-Languages title: Plex Auto Languages train: community -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/validations.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/configs.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/container.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/container.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/depends.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/device.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/device.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/devices.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/dns.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/environment.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/error.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/error.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/functions.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/labels.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/notes.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/portal.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/portals.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/ports.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/render.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/render.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/resources.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/restart.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/storage.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/validations.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/plex-auto-languages/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/plex-auto-languages/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/portainer/app.yaml b/ix-dev/community/portainer/app.yaml index d236472065..a89c032f3e 100644 --- a/ix-dev/community/portainer/app.yaml +++ b/ix-dev/community/portainer/app.yaml @@ -28,8 +28,8 @@ keywords: - docker - compose - container -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -49,4 +49,4 @@ sources: - https://github.com/portainer/portainer title: Portainer train: community -version: 1.3.7 +version: 1.3.8 diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/validations.py b/ix-dev/community/portainer/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/portainer/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/configs.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/container.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/container.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/depends.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deps.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/device.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/device.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/devices.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/dns.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/environment.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/error.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/error.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/functions.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/labels.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/notes.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/portal.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/portals.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/ports.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/render.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/render.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/resources.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/restart.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/storage.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_6/validations.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/portainer/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/portainer/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/portainer/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/portainer/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/portainer/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/postgres/app.yaml b/ix-dev/community/postgres/app.yaml index b1158acede..b974174b7c 100644 --- a/ix-dev/community/postgres/app.yaml +++ b/ix-dev/community/postgres/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/postgres/icons/icon.png keywords: - database -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -27,4 +27,4 @@ sources: - https://hub.docker.com/_/postgres title: Postgres train: community -version: 1.0.11 +version: 1.0.12 diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/validations.py b/ix-dev/community/postgres/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/postgres/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/configs.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/container.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/container.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/depends.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deps.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/device.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/device.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/devices.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/dns.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/environment.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/error.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/error.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/functions.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/labels.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/notes.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/portal.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/portals.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/ports.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/render.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/render.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/resources.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/restart.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/storage.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_6/validations.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/postgres/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/postgres/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/postgres/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/postgres/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/postgres/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/prowlarr/app.yaml b/ix-dev/community/prowlarr/app.yaml index 07ed529de3..c6d8f47179 100644 --- a/ix-dev/community/prowlarr/app.yaml +++ b/ix-dev/community/prowlarr/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/prowlarr/icons/icon.png keywords: - indexer -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://prowlarr.com title: Prowlarr train: community -version: 1.3.9 +version: 1.3.10 diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/prowlarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/prowlarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/prowlarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/prowlarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/qbittorrent/app.yaml b/ix-dev/community/qbittorrent/app.yaml index ac79c33b6a..95038718b4 100644 --- a/ix-dev/community/qbittorrent/app.yaml +++ b/ix-dev/community/qbittorrent/app.yaml @@ -11,8 +11,8 @@ keywords: - media - torrent - download -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://www.qbittorrent.org/ title: qBittorrent train: community -version: 1.1.12 +version: 1.1.13 diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/validations.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/configs.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/container.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/container.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/depends.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/device.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/device.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/devices.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/dns.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/environment.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/error.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/error.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/functions.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/labels.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/notes.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/portal.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/portals.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/ports.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/render.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/render.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/resources.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/restart.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/storage.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/validations.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/qbittorrent/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/qbittorrent/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/radarr/app.yaml b/ix-dev/community/radarr/app.yaml index b159efc9d6..ffb6c08e44 100644 --- a/ix-dev/community/radarr/app.yaml +++ b/ix-dev/community/radarr/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/radarr/icons/icon.png keywords: - media - movies -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/Radarr/Radarr title: Radarr train: community -version: 1.2.6 +version: 1.2.7 diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/radarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/radarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/radarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/radarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/radarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/radarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/radarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/readarr/app.yaml b/ix-dev/community/readarr/app.yaml index 594f853314..60f3777853 100644 --- a/ix-dev/community/readarr/app.yaml +++ b/ix-dev/community/readarr/app.yaml @@ -11,8 +11,8 @@ keywords: - media - ebook - audiobook -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -33,4 +33,4 @@ sources: - https://github.com/Readarr/Readarr title: Readarr train: community -version: 1.1.7 +version: 1.1.8 diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/readarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/readarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/readarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/readarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/readarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/readarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/readarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/recyclarr/app.yaml b/ix-dev/community/recyclarr/app.yaml index a437143108..d187c38919 100644 --- a/ix-dev/community/recyclarr/app.yaml +++ b/ix-dev/community/recyclarr/app.yaml @@ -11,8 +11,8 @@ keywords: - sync - sonarr - radarr -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://github.com/recyclarr/recyclarr/tree/recyclarr title: Recyclarr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/recyclarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/recyclarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/recyclarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/recyclarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/redis/app.yaml b/ix-dev/community/redis/app.yaml index 2ff3528720..fffbb7efa2 100644 --- a/ix-dev/community/redis/app.yaml +++ b/ix-dev/community/redis/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/redis/icons/icon.png keywords: - cache -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://redis.io/ title: Redis train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/validations.py b/ix-dev/community/redis/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/redis/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/redis/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/redis/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/redis/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/configs.py b/ix-dev/community/redis/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/container.py b/ix-dev/community/redis/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/container.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/depends.py b/ix-dev/community/redis/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deps.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/redis/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/device.py b/ix-dev/community/redis/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/device.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/devices.py b/ix-dev/community/redis/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/dns.py b/ix-dev/community/redis/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/environment.py b/ix-dev/community/redis/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/error.py b/ix-dev/community/redis/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/error.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/redis/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/functions.py b/ix-dev/community/redis/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/redis/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/labels.py b/ix-dev/community/redis/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/notes.py b/ix-dev/community/redis/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/portal.py b/ix-dev/community/redis/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/portals.py b/ix-dev/community/redis/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/ports.py b/ix-dev/community/redis/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/render.py b/ix-dev/community/redis/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/render.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/resources.py b/ix-dev/community/redis/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/restart.py b/ix-dev/community/redis/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/storage.py b/ix-dev/community/redis/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/redis/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_6/validations.py b/ix-dev/community/redis/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/redis/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/redis/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/redis/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/redis/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/redis/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/redis/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/redis/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/redis/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/redis/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/redis/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/roundcube/app.yaml b/ix-dev/community/roundcube/app.yaml index 8d7d07277d..def4efad1e 100644 --- a/ix-dev/community/roundcube/app.yaml +++ b/ix-dev/community/roundcube/app.yaml @@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/roundcube/icons/icon.png keywords: - webmail - email -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -49,4 +49,4 @@ sources: - https://hub.docker.com/r/roundcube/roundcubemail/ title: Roundcube train: community -version: 1.2.2 +version: 1.2.3 diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/validations.py b/ix-dev/community/roundcube/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/roundcube/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/configs.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/container.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/container.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/depends.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deps.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/device.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/device.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/devices.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/dns.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/environment.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/error.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/error.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/functions.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/labels.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/notes.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/portal.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/portals.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/ports.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/render.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/render.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/resources.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/restart.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/storage.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_6/validations.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/roundcube/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/roundcube/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/roundcube/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/roundcube/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/roundcube/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/rsyncd/app.yaml b/ix-dev/community/rsyncd/app.yaml index e974fe0b92..06ef02a7a5 100644 --- a/ix-dev/community/rsyncd/app.yaml +++ b/ix-dev/community/rsyncd/app.yaml @@ -22,8 +22,8 @@ keywords: - sync - rsync - file transfer -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -41,4 +41,4 @@ sources: - https://hub.docker.com/r/ixsystems/rsyncd title: Rsync Daemon train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/validations.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/configs.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/container.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/container.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/depends.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/device.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/device.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/devices.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/dns.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/environment.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/error.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/error.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/functions.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/labels.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/notes.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/portal.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/portals.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/ports.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/render.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/render.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/resources.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/restart.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/storage.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_6/validations.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/rsyncd/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/rsyncd/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/rsyncd/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/rsyncd/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/rust-desk/app.yaml b/ix-dev/community/rust-desk/app.yaml index 033dcc2db4..ccdac2351c 100644 --- a/ix-dev/community/rust-desk/app.yaml +++ b/ix-dev/community/rust-desk/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/rust-desk/icons/icon.png keywords: - remote - desktop -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://github.com/rustdesk/rustdesk-server title: Rust Desk train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/validations.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/configs.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/container.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/container.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/depends.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/device.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/device.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/devices.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/dns.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/environment.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/error.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/error.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/functions.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/labels.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/notes.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/portal.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/portals.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/ports.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/render.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/render.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/resources.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/restart.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/storage.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_6/validations.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/rust-desk/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/rust-desk/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/rust-desk/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/rust-desk/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/sabnzbd/app.yaml b/ix-dev/community/sabnzbd/app.yaml index ddb73f0994..736675097e 100644 --- a/ix-dev/community/sabnzbd/app.yaml +++ b/ix-dev/community/sabnzbd/app.yaml @@ -10,8 +10,8 @@ keywords: - media - usenet - newsreader -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://sabnzbd.org/ title: SABnzbd train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/validations.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/configs.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/container.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/container.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/depends.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/device.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/device.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/devices.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/dns.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/environment.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/error.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/error.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/functions.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/labels.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/notes.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/portal.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/portals.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/ports.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/render.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/render.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/resources.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/restart.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/storage.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/validations.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/sabnzbd/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/sabnzbd/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/scrutiny/app.yaml b/ix-dev/community/scrutiny/app.yaml index 99f3b1e8aa..ddf14b7c54 100644 --- a/ix-dev/community/scrutiny/app.yaml +++ b/ix-dev/community/scrutiny/app.yaml @@ -22,8 +22,8 @@ icon: https://media.sys.truenas.net/apps/scrutiny/icons/icon.svg keywords: - disk - monitoring -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -41,4 +41,4 @@ sources: - https://github.com/AnalogJ/scrutiny title: Scrutiny train: community -version: 1.0.9 +version: 1.0.10 diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/validations.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/configs.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/container.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/container.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/depends.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/device.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/device.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/devices.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/dns.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/environment.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/error.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/error.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/functions.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/labels.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/notes.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/portal.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/portals.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/ports.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/render.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/render.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/resources.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/restart.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/storage.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_6/validations.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/scrutiny/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/scrutiny/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/scrutiny/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/scrutiny/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/searxng/app.yaml b/ix-dev/community/searxng/app.yaml index f4a32e5bd5..50fc9a07e1 100644 --- a/ix-dev/community/searxng/app.yaml +++ b/ix-dev/community/searxng/app.yaml @@ -12,8 +12,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/searxng/icons/icon.svg keywords: - search -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/searxng/searxng title: SearXNG train: community -version: 1.1.6 +version: 1.1.7 diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/validations.py b/ix-dev/community/searxng/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/searxng/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/configs.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/container.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/container.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/depends.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deps.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/device.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/device.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/devices.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/dns.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/environment.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/error.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/error.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/functions.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/labels.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/notes.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/portal.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/portals.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/ports.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/render.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/render.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/resources.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/restart.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/storage.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_6/validations.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/searxng/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/searxng/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/searxng/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/searxng/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/searxng/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/sftpgo/app.yaml b/ix-dev/community/sftpgo/app.yaml index 0f6f50dd05..4b6c71e920 100644 --- a/ix-dev/community/sftpgo/app.yaml +++ b/ix-dev/community/sftpgo/app.yaml @@ -9,8 +9,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/sftpgo/icons/icon.png keywords: - sftp -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - https://github.com/drakkan/sftpgo title: SFTPGo train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/validations.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/configs.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/container.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/container.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/depends.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/device.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/device.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/devices.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/dns.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/environment.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/error.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/error.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/functions.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/labels.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/notes.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/portal.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/portals.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/ports.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/render.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/render.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/resources.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/restart.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/storage.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_6/validations.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/sftpgo/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/sftpgo/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/sftpgo/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/sftpgo/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/sonarr/app.yaml b/ix-dev/community/sonarr/app.yaml index 63bd2cfaef..edb9b94aba 100644 --- a/ix-dev/community/sonarr/app.yaml +++ b/ix-dev/community/sonarr/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/sonarr/icons/icon.png keywords: - media - series -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/Sonarr/Sonarr title: Sonarr train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/sonarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/sonarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/sonarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/sonarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/sonarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/sonarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/sonarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/tailscale/app.yaml b/ix-dev/community/tailscale/app.yaml index e4522b5338..81c4b9eb01 100644 --- a/ix-dev/community/tailscale/app.yaml +++ b/ix-dev/community/tailscale/app.yaml @@ -23,8 +23,8 @@ icon: https://media.sys.truenas.net/apps/tailscale/icons/icon.png keywords: - vpn - tailscale -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://hub.docker.com/r/tailscale/tailscale title: Tailscale train: community -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/validations.py b/ix-dev/community/tailscale/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/tailscale/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/configs.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/container.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/container.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/depends.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deps.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/device.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/device.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/devices.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/dns.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/environment.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/error.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/error.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/functions.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/labels.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/notes.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/portal.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/portals.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/ports.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/render.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/render.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/resources.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/restart.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/storage.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_6/validations.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/tailscale/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/tailscale/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/tailscale/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/tailscale/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/tailscale/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/tautulli/app.yaml b/ix-dev/community/tautulli/app.yaml index 8cef62b39e..f85d33a7a8 100644 --- a/ix-dev/community/tautulli/app.yaml +++ b/ix-dev/community/tautulli/app.yaml @@ -11,8 +11,8 @@ keywords: - media - analytics - notifications -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -34,4 +34,4 @@ sources: - https://github.com/Tautulli/Tautulli title: Tautulli train: community -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/validations.py b/ix-dev/community/tautulli/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/tautulli/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/configs.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/container.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/container.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/depends.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deps.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/device.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/device.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/devices.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/dns.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/environment.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/error.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/error.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/functions.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/labels.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/notes.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/portal.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/portals.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/ports.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/render.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/render.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/resources.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/restart.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/storage.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_6/validations.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/tautulli/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/tautulli/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/tautulli/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/tautulli/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/tautulli/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/tdarr/app.yaml b/ix-dev/community/tdarr/app.yaml index 7a31871304..9377402ad2 100644 --- a/ix-dev/community/tdarr/app.yaml +++ b/ix-dev/community/tdarr/app.yaml @@ -18,8 +18,8 @@ icon: https://media.sys.truenas.net/apps/tdarr/icons/icon.png keywords: - encode - transcode -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -40,4 +40,4 @@ sources: - https://docs.tdarr.io/docs title: Tdarr train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/validations.py b/ix-dev/community/tdarr/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/tdarr/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/configs.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/container.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/container.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/depends.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deps.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/device.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/device.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/devices.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/dns.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/environment.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/error.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/error.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/functions.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/labels.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/notes.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/portal.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/portals.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/ports.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/render.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/render.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/resources.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/restart.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/storage.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_6/validations.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/tdarr/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/tdarr/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/tdarr/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/tdarr/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/tdarr/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/terraria/app.yaml b/ix-dev/community/terraria/app.yaml index 5268234eae..dfa1ecde54 100644 --- a/ix-dev/community/terraria/app.yaml +++ b/ix-dev/community/terraria/app.yaml @@ -13,8 +13,8 @@ keywords: - game - terraria - world -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -32,4 +32,4 @@ sources: - https://github.com/ryansheehan/terraria title: Terraria train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/validations.py b/ix-dev/community/terraria/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/terraria/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/configs.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/container.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/container.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/depends.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deps.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/device.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/device.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/devices.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/dns.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/environment.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/error.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/error.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/functions.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/labels.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/notes.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/portal.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/portals.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/ports.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/render.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/render.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/resources.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/restart.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/storage.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_6/validations.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/terraria/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/terraria/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/terraria/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/terraria/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/terraria/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/tftpd-hpa/app.yaml b/ix-dev/community/tftpd-hpa/app.yaml index ff740f605c..7a1c9bceb2 100644 --- a/ix-dev/community/tftpd-hpa/app.yaml +++ b/ix-dev/community/tftpd-hpa/app.yaml @@ -17,8 +17,8 @@ icon: https://media.sys.truenas.net/apps/tftpd-hpa/icons/icon.png keywords: - tftp - netboot -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -36,4 +36,4 @@ sources: - https://hub.docker.com/r/ixsystems/tftpd-hpa title: TFTP Server train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/validations.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/configs.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/container.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/container.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/depends.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/device.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/device.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/devices.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/dns.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/environment.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/error.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/error.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/functions.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/labels.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/notes.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/portal.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/portals.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/ports.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/render.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/render.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/resources.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/restart.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/storage.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/validations.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/tftpd-hpa/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/tftpd-hpa/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/tiny-media-manager/app.yaml b/ix-dev/community/tiny-media-manager/app.yaml index c33ed8e7fb..97454ce1d4 100644 --- a/ix-dev/community/tiny-media-manager/app.yaml +++ b/ix-dev/community/tiny-media-manager/app.yaml @@ -16,8 +16,8 @@ keywords: - media - tv-shows - movies -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -39,4 +39,4 @@ sources: - https://hub.docker.com/r/tinymediamanager/tinymediamanager title: Tiny Media Manager train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/validations.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/configs.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/container.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/container.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/depends.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/device.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/device.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/devices.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/dns.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/environment.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/error.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/error.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/functions.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/labels.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/notes.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/portal.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/portals.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/ports.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/render.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/render.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/resources.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/restart.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/storage.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/validations.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/tiny-media-manager/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/tiny-media-manager/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/transmission/app.yaml b/ix-dev/community/transmission/app.yaml index 378bf306e5..659d508d01 100644 --- a/ix-dev/community/transmission/app.yaml +++ b/ix-dev/community/transmission/app.yaml @@ -10,8 +10,8 @@ keywords: - media - torrent - download -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://transmissionbt.com/ title: Transmission train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/validations.py b/ix-dev/community/transmission/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/transmission/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/configs.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/container.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/container.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/depends.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deps.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/device.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/device.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/devices.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/dns.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/environment.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/error.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/error.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/functions.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/labels.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/notes.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/portal.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/portals.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/ports.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/render.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/render.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/resources.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/restart.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/storage.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_6/validations.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/transmission/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/transmission/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/transmission/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/transmission/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/transmission/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/twofactor-auth/app.yaml b/ix-dev/community/twofactor-auth/app.yaml index fac0fcb4a1..f3248e74f0 100644 --- a/ix-dev/community/twofactor-auth/app.yaml +++ b/ix-dev/community/twofactor-auth/app.yaml @@ -11,8 +11,8 @@ keywords: - security - 2fa - otp -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://hub.docker.com/r/2fauth/2fauth/ title: 2FAuth train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/validations.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/configs.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/container.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/container.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/depends.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/device.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/device.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/devices.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/dns.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/environment.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/error.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/error.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/functions.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/labels.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/notes.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/portal.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/portals.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/ports.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/render.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/render.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/resources.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/restart.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/storage.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/validations.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/twofactor-auth/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/twofactor-auth/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/unifi-controller/app.yaml b/ix-dev/community/unifi-controller/app.yaml index d49081d79b..04119f22bf 100644 --- a/ix-dev/community/unifi-controller/app.yaml +++ b/ix-dev/community/unifi-controller/app.yaml @@ -10,8 +10,8 @@ keywords: - controller - unifi - network -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://hub.docker.com/r/goofball222/unifi title: Unifi Controller train: community -version: 1.3.3 +version: 1.3.4 diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/validations.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/configs.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/container.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/container.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/depends.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/device.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/device.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/devices.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/dns.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/environment.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/error.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/error.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/functions.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/labels.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/notes.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/portal.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/portals.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/ports.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/render.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/render.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/resources.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/restart.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/storage.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/validations.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/unifi-controller/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/unifi-controller/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/unifi-protect-backup/app.yaml b/ix-dev/community/unifi-protect-backup/app.yaml index 6d75ac72f0..e94e257c81 100644 --- a/ix-dev/community/unifi-protect-backup/app.yaml +++ b/ix-dev/community/unifi-protect-backup/app.yaml @@ -18,8 +18,8 @@ icon: https://media.sys.truenas.net/apps/unifi-protect-backup/icons/icon.svg keywords: - backup - unifi-protect -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -37,4 +37,4 @@ sources: - https://github.com/ep1cman/unifi-protect-backup/pkgs/container/unifi-protect-backup title: Unifi Protect Backup train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/validations.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/configs.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/container.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/container.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/depends.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/device.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/device.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/devices.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/dns.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/environment.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/error.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/error.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/functions.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/labels.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/notes.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/portal.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/portals.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/ports.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/render.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/render.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/resources.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/restart.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/storage.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/validations.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/unifi-protect-backup/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/uptime-kuma/app.yaml b/ix-dev/community/uptime-kuma/app.yaml index 73fd495a31..a9f6acc9ea 100644 --- a/ix-dev/community/uptime-kuma/app.yaml +++ b/ix-dev/community/uptime-kuma/app.yaml @@ -11,8 +11,8 @@ icon: https://media.sys.truenas.net/apps/uptime-kuma/icons/icon.svg keywords: - uptime - monitor -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/louislam/uptime-kuma title: Uptime Kuma train: community -version: 1.0.10 +version: 1.0.11 diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/validations.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/configs.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/container.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/container.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/depends.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/device.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/device.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/devices.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/dns.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/environment.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/error.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/error.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/functions.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/labels.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/notes.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/portal.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/portals.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/ports.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/render.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/render.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/resources.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/restart.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/storage.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/validations.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/uptime-kuma/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/uptime-kuma/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/vaultwarden/app.yaml b/ix-dev/community/vaultwarden/app.yaml index 4a08981db0..3ab7427f7f 100644 --- a/ix-dev/community/vaultwarden/app.yaml +++ b/ix-dev/community/vaultwarden/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/vaultwarden/icons/icon.png keywords: - password - manager -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -34,4 +34,4 @@ sources: - https://github.com/dani-garcia/vaultwarden title: Vaultwarden train: community -version: 1.2.5 +version: 1.2.6 diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/validations.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/configs.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/container.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/container.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/depends.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/device.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/device.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/devices.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/dns.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/environment.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/error.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/error.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/functions.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/labels.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/notes.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/portal.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/portals.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/ports.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/render.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/render.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/resources.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/restart.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/storage.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/validations.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/vaultwarden/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/vaultwarden/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/vikunja/app.yaml b/ix-dev/community/vikunja/app.yaml index b04904688b..0b74211d7d 100644 --- a/ix-dev/community/vikunja/app.yaml +++ b/ix-dev/community/vikunja/app.yaml @@ -8,8 +8,8 @@ host_mounts: [] icon: https://media.sys.truenas.net/apps/vikunja/icons/icon.png keywords: - todo -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -46,4 +46,4 @@ sources: - https://vikunja.io/ title: Vikunja train: community -version: 1.4.3 +version: 1.4.4 diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/validations.py b/ix-dev/community/vikunja/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/vikunja/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/configs.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/container.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/container.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/depends.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deps.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/device.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/device.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/devices.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/dns.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/environment.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/error.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/error.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/functions.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/labels.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/notes.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/portal.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/portals.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/ports.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/render.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/render.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/resources.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/restart.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/storage.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_6/validations.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/vikunja/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/vikunja/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/vikunja/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/vikunja/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/vikunja/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/webdav/app.yaml b/ix-dev/community/webdav/app.yaml index ae0d60d4e6..f7c9b12e96 100644 --- a/ix-dev/community/webdav/app.yaml +++ b/ix-dev/community/webdav/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/webdav/icons/icon.png keywords: - webdav - file-sharing -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -28,4 +28,4 @@ sources: - http://www.webdav.org/ title: WebDAV train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/validations.py b/ix-dev/community/webdav/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/webdav/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/configs.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/container.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/container.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/depends.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deps.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/device.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/device.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/devices.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/dns.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/environment.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/error.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/error.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/functions.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/labels.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/notes.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/portal.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/portals.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/ports.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/render.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/render.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/resources.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/restart.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/storage.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_6/validations.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/webdav/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/webdav/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/webdav/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/webdav/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/webdav/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/whoogle/app.yaml b/ix-dev/community/whoogle/app.yaml index aeaf272b73..630e645932 100644 --- a/ix-dev/community/whoogle/app.yaml +++ b/ix-dev/community/whoogle/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/whoogle/icons/icon.png keywords: - search - engine -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://hub.docker.com/r/benbusby/whoogle-search title: Whoogle train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/validations.py b/ix-dev/community/whoogle/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/whoogle/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/configs.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/container.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/container.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/depends.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deps.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/device.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/device.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/devices.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/dns.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/environment.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/error.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/error.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/functions.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/labels.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/notes.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/portal.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/portals.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/ports.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/render.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/render.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/resources.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/restart.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/storage.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_6/validations.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/whoogle/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/whoogle/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/whoogle/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/whoogle/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/whoogle/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/wordpress/app.yaml b/ix-dev/community/wordpress/app.yaml index d03979bfae..1d5c33b4bc 100644 --- a/ix-dev/community/wordpress/app.yaml +++ b/ix-dev/community/wordpress/app.yaml @@ -11,8 +11,8 @@ icon: https://media.sys.truenas.net/apps/wordpress/icons/icon.png keywords: - cms - blog -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -38,4 +38,4 @@ sources: - https://hub.docker.com/_/wordpress title: Wordpress train: community -version: 1.1.3 +version: 1.1.4 diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/validations.py b/ix-dev/community/wordpress/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/wordpress/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/configs.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/container.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/container.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/depends.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deps.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/device.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/device.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/devices.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/dns.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/environment.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/error.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/error.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/functions.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/labels.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/notes.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/portal.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/portals.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/ports.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/render.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/render.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/resources.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/restart.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/storage.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_6/validations.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/wordpress/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/wordpress/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/wordpress/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/wordpress/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/wordpress/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/community/zerotier/app.yaml b/ix-dev/community/zerotier/app.yaml index eeae0e55fe..e75b60cf87 100644 --- a/ix-dev/community/zerotier/app.yaml +++ b/ix-dev/community/zerotier/app.yaml @@ -34,8 +34,8 @@ icon: https://media.sys.truenas.net/apps/zerotier/icons/icon.png keywords: - vpn - zerotier -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -53,4 +53,4 @@ sources: - https://hub.docker.com/r/zerotier/zerotier title: Zerotier train: community -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/validations.py b/ix-dev/community/zerotier/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/community/zerotier/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/__init__.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/__init__.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/configs.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/configs.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/container.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/container.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/depends.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/depends.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deploy.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deploy.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deps.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deps.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/device.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/device.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/devices.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/devices.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/dns.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/dns.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/environment.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/environment.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/error.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/error.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/formatter.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/formatter.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/functions.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/functions.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/labels.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/labels.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/notes.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/notes.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/portal.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/portal.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/portals.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/portals.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/ports.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/ports.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/render.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/render.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/resources.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/resources.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/restart.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/restart.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/storage.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/storage.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/sysctls.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_6/validations.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/community/zerotier/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_types.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/community/zerotier/templates/library/base_v2_1_5/volumes.py b/ix-dev/community/zerotier/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/community/zerotier/templates/library/base_v2_1_5/volumes.py rename to ix-dev/community/zerotier/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/enterprise/asigra-ds-system/app.yaml b/ix-dev/enterprise/asigra-ds-system/app.yaml index ade0f3cc3b..9da437e512 100644 --- a/ix-dev/enterprise/asigra-ds-system/app.yaml +++ b/ix-dev/enterprise/asigra-ds-system/app.yaml @@ -19,8 +19,8 @@ keywords: - backup - restore - asigra -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -47,4 +47,4 @@ sources: - https://hub.docker.com/r/asigra/ds-system title: Asigra DS-System train: enterprise -version: 1.0.20 +version: 1.0.21 diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/validations.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/__init__.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/__init__.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/configs.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/configs.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/container.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/container.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/depends.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/depends.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deploy.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deploy.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/device.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/device.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/devices.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/devices.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/dns.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/dns.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/environment.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/environment.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/error.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/error.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/formatter.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/formatter.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/functions.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/functions.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/labels.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/labels.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/notes.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/notes.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/portal.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/portal.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/portals.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/portals.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/ports.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/ports.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/render.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/render.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/resources.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/resources.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/restart.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/restart.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/storage.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/storage.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/sysctls.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/validations.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_types.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volumes.py b/ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_5/volumes.py rename to ix-dev/enterprise/asigra-ds-system/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/enterprise/minio/app.yaml b/ix-dev/enterprise/minio/app.yaml index 291712f83f..40ba99ba3e 100644 --- a/ix-dev/enterprise/minio/app.yaml +++ b/ix-dev/enterprise/minio/app.yaml @@ -11,8 +11,8 @@ keywords: - minio - cloud - s3 -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/minio/minio title: MinIO train: enterprise -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/validations.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/__init__.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/__init__.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/configs.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/configs.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/container.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/container.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/depends.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/depends.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deploy.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deploy.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/device.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/device.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/devices.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/devices.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/dns.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/dns.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/environment.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/environment.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/error.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/error.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/formatter.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/formatter.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/functions.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/functions.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/labels.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/labels.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/notes.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/notes.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/portal.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/portal.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/portals.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/portals.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/ports.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/ports.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/render.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/render.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/resources.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/resources.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/restart.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/restart.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/storage.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/storage.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/sysctls.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_6/validations.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_types.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/enterprise/minio/templates/library/base_v2_1_5/volumes.py b/ix-dev/enterprise/minio/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/enterprise/minio/templates/library/base_v2_1_5/volumes.py rename to ix-dev/enterprise/minio/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/enterprise/syncthing/app.yaml b/ix-dev/enterprise/syncthing/app.yaml index 1a41a01f32..d3c33945ab 100644 --- a/ix-dev/enterprise/syncthing/app.yaml +++ b/ix-dev/enterprise/syncthing/app.yaml @@ -25,8 +25,8 @@ icon: https://media.sys.truenas.net/apps/syncthing/icons/icon.svg keywords: - sync - file-sharing -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -49,4 +49,4 @@ sources: - https://hub.docker.com/r/syncthing/syncthing title: Syncthing train: enterprise -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/validations.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/__init__.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/__init__.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/configs.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/configs.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/container.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/container.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/depends.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/depends.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deploy.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deploy.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/device.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/device.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/devices.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/devices.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/dns.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/dns.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/environment.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/environment.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/error.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/error.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/formatter.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/formatter.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/functions.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/functions.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/labels.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/labels.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/notes.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/notes.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/portal.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/portal.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/portals.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/portals.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/ports.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/ports.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/render.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/render.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/resources.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/resources.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/restart.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/restart.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/storage.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/storage.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/sysctls.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/validations.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_types.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volumes.py b/ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/enterprise/syncthing/templates/library/base_v2_1_5/volumes.py rename to ix-dev/enterprise/syncthing/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/collabora/app.yaml b/ix-dev/stable/collabora/app.yaml index 1dd045ea02..ad78c1a233 100644 --- a/ix-dev/stable/collabora/app.yaml +++ b/ix-dev/stable/collabora/app.yaml @@ -27,8 +27,8 @@ keywords: - office - documents - productivity -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -53,4 +53,4 @@ sources: - https://hub.docker.com/r/collabora/code title: Collabora train: stable -version: 1.2.5 +version: 1.2.6 diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/collabora/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/collabora/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/container.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/device.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/error.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/render.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/collabora/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/collabora/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/collabora/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/collabora/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/collabora/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/diskoverdata/app.yaml b/ix-dev/stable/diskoverdata/app.yaml index 095d13c299..0178978095 100644 --- a/ix-dev/stable/diskoverdata/app.yaml +++ b/ix-dev/stable/diskoverdata/app.yaml @@ -23,8 +23,8 @@ keywords: - monitoring - management - discovery -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -50,4 +50,4 @@ sources: - https://github.com/linuxserver/docker-diskover title: Diskover Data train: stable -version: 1.4.4 +version: 1.4.5 diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/container.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/device.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/error.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/render.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/diskoverdata/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/diskoverdata/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/elastic-search/app.yaml b/ix-dev/stable/elastic-search/app.yaml index f4b210cc53..9796fff998 100644 --- a/ix-dev/stable/elastic-search/app.yaml +++ b/ix-dev/stable/elastic-search/app.yaml @@ -10,8 +10,8 @@ icon: https://media.sys.truenas.net/apps/elastic-search/icons/icon.svg keywords: - search - elastic -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -29,4 +29,4 @@ sources: - https://www.elastic.co/guide/en/elasticsearch/reference/master/docker.html#docker-configuration-methods title: Elastic Search train: stable -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/container.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/device.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/error.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/render.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/elastic-search/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/elastic-search/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/emby/app.yaml b/ix-dev/stable/emby/app.yaml index 2d5745665c..e5fdb54a45 100644 --- a/ix-dev/stable/emby/app.yaml +++ b/ix-dev/stable/emby/app.yaml @@ -27,8 +27,8 @@ keywords: - series - tv - streaming -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -49,4 +49,4 @@ sources: - https://github.com/truenas/charts/tree/master/charts/emby title: Emby Server train: stable -version: 1.2.7 +version: 1.2.8 diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/emby/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/emby/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/container.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/device.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/error.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/render.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/emby/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/emby/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/emby/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/emby/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/emby/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/home-assistant/app.yaml b/ix-dev/stable/home-assistant/app.yaml index a8d9f83707..904f7da2db 100644 --- a/ix-dev/stable/home-assistant/app.yaml +++ b/ix-dev/stable/home-assistant/app.yaml @@ -20,8 +20,8 @@ icon: https://media.sys.truenas.net/apps/home-assistant/icons/icon.png keywords: - home-automation - assistant -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -45,4 +45,4 @@ sources: - https://github.com/truenas/charts/tree/master/charts/home-assistant title: Home Assistant train: stable -version: 1.4.7 +version: 1.4.8 diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/container.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/device.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/error.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/render.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/home-assistant/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/home-assistant/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/ix-app/app.yaml b/ix-dev/stable/ix-app/app.yaml index 213ad5f535..086a8c433a 100644 --- a/ix-dev/stable/ix-app/app.yaml +++ b/ix-dev/stable/ix-app/app.yaml @@ -7,8 +7,8 @@ home: https://www.truenas.com/ host_mounts: [] icon: https://media.sys.truenas.net/apps/ix-chart/icons/icon.webp keywords: [] -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -19,4 +19,4 @@ screenshots: [] sources: [] title: iX App train: stable -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/container.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/device.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/error.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/render.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/ix-app/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/ix-app/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/ix-app/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/ix-app/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/minio/app.yaml b/ix-dev/stable/minio/app.yaml index 8f4a8e986b..04487c5def 100644 --- a/ix-dev/stable/minio/app.yaml +++ b/ix-dev/stable/minio/app.yaml @@ -10,8 +10,8 @@ keywords: - storage - object-storage - S3 -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -31,4 +31,4 @@ sources: - https://github.com/minio/minio title: MinIO train: stable -version: 1.2.6 +version: 1.2.7 diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/minio/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/minio/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/container.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/device.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/error.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/render.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/minio/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/minio/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/minio/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/minio/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/minio/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/netdata/app.yaml b/ix-dev/stable/netdata/app.yaml index 4462792a16..8f450f4ac4 100644 --- a/ix-dev/stable/netdata/app.yaml +++ b/ix-dev/stable/netdata/app.yaml @@ -34,8 +34,8 @@ keywords: - alerting - metric - monitoring -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -58,4 +58,4 @@ sources: - https://github.com/netdata/netdata title: Netdata train: stable -version: 1.2.4 +version: 1.2.5 diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/netdata/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/netdata/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/container.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/device.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/error.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/render.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/netdata/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/netdata/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/netdata/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/netdata/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/netdata/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/nextcloud/app.yaml b/ix-dev/stable/nextcloud/app.yaml index 4ce1139e92..474d7429b0 100644 --- a/ix-dev/stable/nextcloud/app.yaml +++ b/ix-dev/stable/nextcloud/app.yaml @@ -28,8 +28,8 @@ keywords: - http - web - php -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -66,4 +66,4 @@ sources: - https://github.com/truenas/charts/tree/master/charts/nextcloud title: Nextcloud train: stable -version: 1.5.7 +version: 1.5.8 diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/container.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/device.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/error.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/render.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/nextcloud/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/nextcloud/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/photoprism/app.yaml b/ix-dev/stable/photoprism/app.yaml index bb9c03e6fc..fafef38de7 100644 --- a/ix-dev/stable/photoprism/app.yaml +++ b/ix-dev/stable/photoprism/app.yaml @@ -22,8 +22,8 @@ keywords: - media - photos - image -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -42,4 +42,4 @@ sources: - https://photoprism.app/ title: Photoprism train: stable -version: 1.2.5 +version: 1.2.6 diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/container.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/device.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/error.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/render.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/photoprism/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/photoprism/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/photoprism/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/photoprism/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/pihole/app.yaml b/ix-dev/stable/pihole/app.yaml index 0d17000f56..d92177bbf7 100644 --- a/ix-dev/stable/pihole/app.yaml +++ b/ix-dev/stable/pihole/app.yaml @@ -33,8 +33,8 @@ icon: https://media.sys.truenas.net/apps/pihole/icons/icon.png keywords: - networking - dns -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -53,4 +53,4 @@ sources: - https://github.com/truenas/charts/tree/master/charts/pihole title: Pi-hole train: stable -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/pihole/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/pihole/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/container.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/device.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/error.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/render.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/pihole/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/pihole/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/pihole/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/pihole/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/pihole/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/plex/app.yaml b/ix-dev/stable/plex/app.yaml index e03ed58520..378a48a682 100644 --- a/ix-dev/stable/plex/app.yaml +++ b/ix-dev/stable/plex/app.yaml @@ -27,8 +27,8 @@ keywords: - series - tv - streaming -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -48,4 +48,4 @@ sources: - https://hub.docker.com/r/plexinc/pms-docker title: Plex train: stable -version: 1.1.9 +version: 1.1.10 diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/plex/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/plex/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/container.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/device.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/error.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/render.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/plex/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/plex/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/plex/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/plex/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/plex/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/prometheus/app.yaml b/ix-dev/stable/prometheus/app.yaml index f4e0cf345c..b21f158249 100644 --- a/ix-dev/stable/prometheus/app.yaml +++ b/ix-dev/stable/prometheus/app.yaml @@ -9,8 +9,8 @@ icon: https://media.sys.truenas.net/apps/prometheus/icons/icon.png keywords: - metrics - prometheus -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -30,4 +30,4 @@ sources: - https://prometheus.io title: Prometheus train: stable -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/container.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/device.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/error.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/render.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/prometheus/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/prometheus/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/prometheus/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/prometheus/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/storj/app.yaml b/ix-dev/stable/storj/app.yaml index ee0399f01d..1ddc8af123 100644 --- a/ix-dev/stable/storj/app.yaml +++ b/ix-dev/stable/storj/app.yaml @@ -18,8 +18,8 @@ keywords: - networking - financial - file-sharing -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -37,4 +37,4 @@ sources: - https://www.storj.io title: Storj train: stable -version: 1.2.3 +version: 1.2.4 diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/storj/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/storj/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/container.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/device.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/error.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/render.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/storj/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/storj/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/storj/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/storj/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/storj/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/syncthing/app.yaml b/ix-dev/stable/syncthing/app.yaml index d66f9ddf0a..aeadefb049 100644 --- a/ix-dev/stable/syncthing/app.yaml +++ b/ix-dev/stable/syncthing/app.yaml @@ -26,8 +26,8 @@ keywords: - sync - file-sharing - backup -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -50,4 +50,4 @@ sources: - https://hub.docker.com/r/syncthing/syncthing title: Syncthing train: stable -version: 1.1.4 +version: 1.1.5 diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/container.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/device.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/error.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/render.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/syncthing/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/syncthing/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/syncthing/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/syncthing/templates/library/base_v2_1_6/volumes.py diff --git a/ix-dev/stable/wg-easy/app.yaml b/ix-dev/stable/wg-easy/app.yaml index daa38dcc3a..1e3e42c66a 100644 --- a/ix-dev/stable/wg-easy/app.yaml +++ b/ix-dev/stable/wg-easy/app.yaml @@ -16,8 +16,8 @@ keywords: - wireguard - network - vpn -lib_version: 2.1.5 -lib_version_hash: 94754830801a8fa90e04e35d324a34a51b90d5919e544ebc1018e065adb02a12 +lib_version: 2.1.6 +lib_version_hash: 84c965e8b9bea696765ab62b8ee3238162fe7807d0f0a61cf9c153994a47fa90 maintainers: - email: dev@ixsystems.com name: truenas @@ -35,4 +35,4 @@ sources: - https://github.com/wg-easy/wg-easy title: WG Easy train: stable -version: 1.1.5 +version: 1.1.6 diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_validations.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_validations.py deleted file mode 100644 index 27fc0e903b..0000000000 --- a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_validations.py +++ /dev/null @@ -1,126 +0,0 @@ -import pytest -from unittest.mock import patch - -from pathlib import Path -from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN - - -def mock_resolve(self): - # Don't modify paths that are from RESTRICTED list initialization - if str(self) in [str(p) for p in RESTRICTED]: - return self - - # For symlinks that point to restricted paths, return the target path - # without stripping /private/ - if str(self).endswith("symlink_restricted"): - return Path("/home") # Return the actual restricted target - - # For other paths, strip /private/ if present - return Path(str(self).removeprefix("/private/")) - - -@pytest.mark.parametrize( - "test_path, expected", - [ - # Non-restricted path (should be valid) - ("/tmp/somefile", True), - # Exactly /mnt (restricted_in) - ("/mnt", False), - # Exactly / (restricted_in) - ("/", False), - # Subdirectory inside /mnt/.ix-apps (restricted) - ("/mnt/.ix-apps/something", False), - # A path that is a restricted directory exactly - ("/home", False), - ("/var/log", False), - ("/mnt/.ix-apps", False), - ("/data", False), - # Subdirectory inside e.g. /data - ("/data/subdir", False), - # Not an obviously restricted path - ("/usr/local/share", True), - # Another system path likely not in restricted list - ("/opt/myapp", True), - ], -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_direct(test_path, expected): - """Test direct paths against the is_allowed_path function.""" - assert is_allowed_path(test_path) == expected - - -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_symlink(tmp_path): - """ - Test that a symlink pointing to a restricted directory is detected as invalid, - and a symlink pointing to an allowed directory is valid. - """ - # Create a real (allowed) directory and a restricted directory in a temp location - allowed_dir = tmp_path / "allowed_dir" - allowed_dir.mkdir() - - restricted_dir = tmp_path / "restricted_dir" - restricted_dir.mkdir() - - # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" - # or we create a subdir to match the restricted pattern. - # For demonstration, let's just patch it to a path in the restricted list. - real_restricted_path = Path("/home") # This is one of the restricted directories - - # Create symlinks to test - symlink_allowed = tmp_path / "symlink_allowed" - symlink_restricted = tmp_path / "symlink_restricted" - - # Point the symlinks - symlink_allowed.symlink_to(allowed_dir) - symlink_restricted.symlink_to(real_restricted_path) - - assert is_allowed_path(str(symlink_allowed)) is True - assert is_allowed_path(str(symlink_restricted)) is False - - -def test_is_allowed_path_nested_symlink(tmp_path): - """ - Test that even a nested symlink that eventually resolves into restricted - directories is seen as invalid. - """ - # e.g., Create 2 symlinks that chain to /root - link1 = tmp_path / "link1" - link2 = tmp_path / "link2" - - # link2 -> /root - link2.symlink_to(Path("/root")) - # link1 -> link2 - link1.symlink_to(link2) - - assert is_allowed_path(str(link1)) is False - - -def test_is_allowed_path_nonexistent(tmp_path): - """ - Test a path that does not exist at all. The code calls .resolve() which will - give the absolute path, but if it's not restricted, it should still be valid. - """ - nonexistent = tmp_path / "this_does_not_exist" - assert is_allowed_path(str(nonexistent)) is True - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED), -) -@patch.object(Path, "resolve", mock_resolve) -def test_is_allowed_path_restricted_list(test_path): - """Test that all items in the RESTRICTED list are invalid.""" - assert is_allowed_path(test_path) is False - - -@pytest.mark.parametrize( - "test_path", - list(RESTRICTED_IN), -) -def test_is_allowed_path_restricted_in_list(test_path): - """ - Test that items in RESTRICTED_IN are invalid. - """ - assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/validations.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/validations.py deleted file mode 100644 index 4c7065c1c7..0000000000 --- a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/validations.py +++ /dev/null @@ -1,271 +0,0 @@ -import re -import ipaddress -from pathlib import Path - - -try: - from .error import RenderError -except ImportError: - from error import RenderError - -OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") -RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) -RESTRICTED: tuple[Path, ...] = ( - Path("/mnt/.ix-apps"), - Path("/data"), - Path("/var/db"), - Path("/root"), - Path("/conf"), - Path("/audit"), - Path("/var/run/middleware"), - Path("/home"), - Path("/boot"), - Path("/var/log"), -) - - -def valid_pull_policy_or_raise(pull_policy: str): - valid_policies = ("missing", "always", "never", "build") - if pull_policy not in valid_policies: - raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") - return pull_policy - - -def valid_sysctl_or_raise(sysctl: str, host_network: bool): - if not sysctl: - raise RenderError("Sysctl cannot be empty") - if host_network and sysctl.startswith("net."): - raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") - - valid_sysctls = [ - "kernel.msgmax", - "kernel.msgmnb", - "kernel.msgmni", - "kernel.sem", - "kernel.shmall", - "kernel.shmmax", - "kernel.shmmni", - "kernel.shm_rmid_forced", - ] - # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls - if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: - raise RenderError( - f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" - ) - return sysctl - - -def valid_redis_password_or_raise(password: str): - forbidden_chars = [" ", "'"] - for char in forbidden_chars: - if char in password: - raise RenderError(f"Redis password cannot contain [{char}]") - - -def valid_octal_mode_or_raise(mode: str): - mode = str(mode) - if not OCTAL_MODE_REGEX.match(mode): - raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") - return mode - - -def valid_host_path_propagation(propagation: str): - valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") - if propagation not in valid_propagations: - raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") - return propagation - - -def valid_portal_scheme_or_raise(scheme: str): - schemes = ("http", "https") - if scheme not in schemes: - raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") - return scheme - - -def valid_port_or_raise(port: int): - if port < 1 or port > 65535: - raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") - return port - - -def valid_ip_or_raise(ip: str): - try: - ipaddress.ip_address(ip) - except ValueError: - raise RenderError(f"Invalid IP address [{ip}]") - return ip - - -def valid_port_mode_or_raise(mode: str): - modes = ("ingress", "host") - if mode not in modes: - raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") - return mode - - -def valid_port_protocol_or_raise(protocol: str): - protocols = ("tcp", "udp") - if protocol not in protocols: - raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") - return protocol - - -def valid_depend_condition_or_raise(condition: str): - valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") - if condition not in valid_conditions: - raise RenderError( - f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" - ) - return condition - - -def valid_cgroup_perm_or_raise(cgroup_perm: str): - valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") - if cgroup_perm not in valid_cgroup_perms: - raise RenderError( - f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" - ) - return cgroup_perm - - -def allowed_dns_opt_or_raise(dns_opt: str): - disallowed_dns_opts = [] - if dns_opt in disallowed_dns_opts: - raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") - return dns_opt - - -def valid_http_path_or_raise(path: str): - path = _valid_path_or_raise(path) - return path - - -def valid_fs_path_or_raise(path: str): - # There is no reason to allow / as a path, - # either on host or in a container side. - if path == "/": - raise RenderError(f"Path [{path}] cannot be [/]") - path = _valid_path_or_raise(path) - return path - - -def is_allowed_path(input_path: str) -> bool: - """ - Validates that the given path (after resolving symlinks) is not - one of the restricted paths or within those restricted directories. - - Returns True if the path is allowed, False otherwise. - """ - # Resolve the path to avoid symlink bypasses - real_path = Path(input_path).resolve() - for restricted in RESTRICTED: - if real_path.is_relative_to(restricted): - return False - - return real_path not in RESTRICTED_IN - - -def allowed_fs_host_path_or_raise(path: str): - if not is_allowed_path(path): - raise RenderError(f"Path [{path}] is not allowed to be mounted.") - return path - - -def _valid_path_or_raise(path: str): - if path == "": - raise RenderError(f"Path [{path}] cannot be empty") - if not path.startswith("/"): - raise RenderError(f"Path [{path}] must start with /") - if "//" in path: - raise RenderError(f"Path [{path}] cannot contain [//]") - return path - - -def allowed_device_or_raise(path: str): - disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] - if path in disallowed_devices: - raise RenderError(f"Device [{path}] is not allowed to be manually added.") - return path - - -def valid_network_mode_or_raise(mode: str, containers: list[str]): - valid_modes = ("host", "none") - if mode in valid_modes: - return mode - - if mode.startswith("service:"): - if mode[8:] not in containers: - raise RenderError(f"Service [{mode[8:]}] not found") - return mode - - raise RenderError( - f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" - ) - - -def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): - valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") - if policy not in valid_restart_policies: - raise RenderError( - f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" - ) - if policy != "on-failure" and maximum_retry_count != 0: - raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") - - if maximum_retry_count < 0: - raise RenderError("Maximum retry count must be a positive integer") - - return policy - - -def valid_cap_or_raise(cap: str): - valid_policies = ( - "ALL", - "AUDIT_CONTROL", - "AUDIT_READ", - "AUDIT_WRITE", - "BLOCK_SUSPEND", - "BPF", - "CHECKPOINT_RESTORE", - "CHOWN", - "DAC_OVERRIDE", - "DAC_READ_SEARCH", - "FOWNER", - "FSETID", - "IPC_LOCK", - "IPC_OWNER", - "KILL", - "LEASE", - "LINUX_IMMUTABLE", - "MAC_ADMIN", - "MAC_OVERRIDE", - "MKNOD", - "NET_ADMIN", - "NET_BIND_SERVICE", - "NET_BROADCAST", - "NET_RAW", - "PERFMON", - "SETFCAP", - "SETGID", - "SETPCAP", - "SETUID", - "SYS_ADMIN", - "SYS_BOOT", - "SYS_CHROOT", - "SYS_MODULE", - "SYS_NICE", - "SYS_PACCT", - "SYS_PTRACE", - "SYS_RAWIO", - "SYS_RESOURCE", - "SYS_TIME", - "SYS_TTY_CONFIG", - "SYSLOG", - "WAKE_ALARM", - ) - - if cap not in valid_policies: - raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") - - return cap diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_sources.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_sources.py deleted file mode 100644 index 030ccd397b..0000000000 --- a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig - -try: - from .error import RenderError - from .formatter import get_hashed_name_for_volume - from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise -except ImportError: - from error import RenderError - from formatter import get_hashed_name_for_volume - from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise - - -class HostPathSource: - def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [host_path_config] to be set for [host_path] type.") - - path = "" - if config.get("acl_enable", False): - acl_path = config.get("acl", {}).get("path") - if not acl_path: - raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") - path = valid_fs_path_or_raise(acl_path) - else: - path = valid_fs_path_or_raise(config.get("path", "")) - - path = path.rstrip("/") - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class IxVolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") - dataset_name = config.get("dataset_name") - if not dataset_name: - raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") - - ix_volumes = self._render_instance.values.get("ix_volumes", {}) - if dataset_name not in ix_volumes: - available = ", ".join(ix_volumes.keys()) - raise RenderError( - f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " - f"Available keys: [{available}]." - ) - - path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) - self.source = allowed_fs_host_path_or_raise(path) - - def get(self): - return self.source - - -class CifsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [cifs_config] to be set for [cifs] type.") - self.source = get_hashed_name_for_volume("cifs", config) - - def get(self): - return self.source - - -class NfsSource: - def __init__(self, render_instance: "Render", config: dict): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [nfs_config] to be set for [nfs] type.") - self.source = get_hashed_name_for_volume("nfs", config) - - def get(self): - return self.source - - -class VolumeSource: - def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): - self._render_instance = render_instance - self.source: str = "" - - if not config: - raise RenderError("Expected [volume_config] to be set for [volume] type.") - - volume_name: str = config.get("volume_name", "") - if not volume_name: - raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") - - self.source = volume_name - - def get(self): - return self.source diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/__init__.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/__init__.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/__init__.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/__init__.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/configs.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/configs.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/configs.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/configs.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/container.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/container.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/container.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/container.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/depends.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/depends.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/depends.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/depends.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deploy.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deploy.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deploy.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deploy.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_mariadb.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_mariadb.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_mariadb.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_mariadb.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_perms.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_perms.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_perms.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_perms.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_postgres.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_postgres.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_postgres.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_postgres.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_redis.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_redis.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/deps_redis.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/deps_redis.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/device.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/device.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/device.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/device.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/devices.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/devices.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/devices.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/devices.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/dns.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/dns.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/dns.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/dns.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/environment.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/environment.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/environment.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/environment.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/error.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/error.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/error.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/error.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/formatter.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/formatter.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/formatter.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/formatter.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/functions.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/functions.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/functions.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/functions.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/healthcheck.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/healthcheck.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/healthcheck.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/healthcheck.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/labels.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/labels.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/labels.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/labels.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/notes.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/notes.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/notes.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/notes.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/portal.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/portal.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/portal.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/portal.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/portals.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/portals.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/portals.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/portals.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/ports.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/ports.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/ports.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/ports.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/render.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/render.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/render.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/render.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/resources.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/resources.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/resources.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/resources.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/restart.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/restart.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/restart.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/restart.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/storage.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/storage.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/storage.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/storage.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/sysctls.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/sysctls.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/sysctls.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/sysctls.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/__init__.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/__init__.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/__init__.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/__init__.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_build_image.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_build_image.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_build_image.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_build_image.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_configs.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_configs.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_configs.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_configs.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_container.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_container.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_container.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_container.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_depends.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_depends.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_depends.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_depends.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_deps.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_deps.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_deps.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_deps.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_device.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_device.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_device.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_device.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_dns.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_dns.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_dns.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_dns.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_environment.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_environment.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_environment.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_environment.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_formatter.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_formatter.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_formatter.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_formatter.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_functions.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_functions.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_functions.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_functions.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_healthcheck.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_healthcheck.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_healthcheck.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_healthcheck.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_labels.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_labels.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_labels.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_labels.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_notes.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_notes.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_notes.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_notes.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_portal.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_portal.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_portal.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_portal.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_ports.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_ports.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_ports.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_ports.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_render.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_render.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_render.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_render.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_resources.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_resources.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_resources.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_resources.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_restart.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_restart.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_restart.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_restart.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_sysctls.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_sysctls.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_sysctls.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_sysctls.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_validations.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_validations.py new file mode 100644 index 0000000000..f0986ce9a5 --- /dev/null +++ b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_validations.py @@ -0,0 +1,132 @@ +import pytest +from unittest.mock import patch + +from pathlib import Path +from validations import is_allowed_path, RESTRICTED, RESTRICTED_IN + + +def mock_resolve(self): + # Don't modify paths that are from RESTRICTED list initialization + if str(self) in [str(p) for p in RESTRICTED]: + return self + + # For symlinks that point to restricted paths, return the target path + # without stripping /private/ + if str(self).endswith("symlink_restricted"): + return Path("/home") # Return the actual restricted target + + # For other paths, strip /private/ if present + return Path(str(self).removeprefix("/private/")) + + +@pytest.mark.parametrize( + "test_path, expected", + [ + # Non-restricted path (should be valid) + ("/tmp/somefile", True), + # Exactly /mnt (restricted_in) + ("/mnt", False), + # Exactly / (restricted_in) + ("/", False), + # Subdirectory inside /mnt/.ix-apps (restricted) + ("/mnt/.ix-apps/something", False), + # A path that is a restricted directory exactly + ("/home", False), + ("/var/log", False), + ("/mnt/.ix-apps", False), + ("/data", False), + # Subdirectory inside e.g. /data + ("/data/subdir", False), + # Not an obviously restricted path + ("/usr/local/share", True), + # Another system path likely not in restricted list + ("/opt/myapp", True), + ], +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_direct(test_path, expected): + """Test direct paths against the is_allowed_path function.""" + assert is_allowed_path(test_path) == expected + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_ix_volume(): + """Test that IX volumes are not allowed""" + assert is_allowed_path("/mnt/.ix-apps/something", True) + + +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_symlink(tmp_path): + """ + Test that a symlink pointing to a restricted directory is detected as invalid, + and a symlink pointing to an allowed directory is valid. + """ + # Create a real (allowed) directory and a restricted directory in a temp location + allowed_dir = tmp_path / "allowed_dir" + allowed_dir.mkdir() + + restricted_dir = tmp_path / "restricted_dir" + restricted_dir.mkdir() + + # We will simulate that "restricted_dir" is actually a symlink link pointing to e.g. "/var/log" + # or we create a subdir to match the restricted pattern. + # For demonstration, let's just patch it to a path in the restricted list. + real_restricted_path = Path("/home") # This is one of the restricted directories + + # Create symlinks to test + symlink_allowed = tmp_path / "symlink_allowed" + symlink_restricted = tmp_path / "symlink_restricted" + + # Point the symlinks + symlink_allowed.symlink_to(allowed_dir) + symlink_restricted.symlink_to(real_restricted_path) + + assert is_allowed_path(str(symlink_allowed)) is True + assert is_allowed_path(str(symlink_restricted)) is False + + +def test_is_allowed_path_nested_symlink(tmp_path): + """ + Test that even a nested symlink that eventually resolves into restricted + directories is seen as invalid. + """ + # e.g., Create 2 symlinks that chain to /root + link1 = tmp_path / "link1" + link2 = tmp_path / "link2" + + # link2 -> /root + link2.symlink_to(Path("/root")) + # link1 -> link2 + link1.symlink_to(link2) + + assert is_allowed_path(str(link1)) is False + + +def test_is_allowed_path_nonexistent(tmp_path): + """ + Test a path that does not exist at all. The code calls .resolve() which will + give the absolute path, but if it's not restricted, it should still be valid. + """ + nonexistent = tmp_path / "this_does_not_exist" + assert is_allowed_path(str(nonexistent)) is True + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED), +) +@patch.object(Path, "resolve", mock_resolve) +def test_is_allowed_path_restricted_list(test_path): + """Test that all items in the RESTRICTED list are invalid.""" + assert is_allowed_path(test_path) is False + + +@pytest.mark.parametrize( + "test_path", + list(RESTRICTED_IN), +) +def test_is_allowed_path_restricted_in_list(test_path): + """ + Test that items in RESTRICTED_IN are invalid. + """ + assert is_allowed_path(test_path) is False diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_volumes.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_volumes.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/tests/test_volumes.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/tests/test_volumes.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/validations.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/validations.py new file mode 100644 index 0000000000..b0a761238f --- /dev/null +++ b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/validations.py @@ -0,0 +1,271 @@ +import re +import ipaddress +from pathlib import Path + + +try: + from .error import RenderError +except ImportError: + from error import RenderError + +OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$") +RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/")) +RESTRICTED: tuple[Path, ...] = ( + Path("/mnt/.ix-apps"), + Path("/data"), + Path("/var/db"), + Path("/root"), + Path("/conf"), + Path("/audit"), + Path("/var/run/middleware"), + Path("/home"), + Path("/boot"), + Path("/var/log"), +) + + +def valid_pull_policy_or_raise(pull_policy: str): + valid_policies = ("missing", "always", "never", "build") + if pull_policy not in valid_policies: + raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]") + return pull_policy + + +def valid_sysctl_or_raise(sysctl: str, host_network: bool): + if not sysctl: + raise RenderError("Sysctl cannot be empty") + if host_network and sysctl.startswith("net."): + raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled") + + valid_sysctls = [ + "kernel.msgmax", + "kernel.msgmnb", + "kernel.msgmni", + "kernel.sem", + "kernel.shmall", + "kernel.shmmax", + "kernel.shmmni", + "kernel.shm_rmid_forced", + ] + # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls: + raise RenderError( + f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]" + ) + return sysctl + + +def valid_redis_password_or_raise(password: str): + forbidden_chars = [" ", "'"] + for char in forbidden_chars: + if char in password: + raise RenderError(f"Redis password cannot contain [{char}]") + + +def valid_octal_mode_or_raise(mode: str): + mode = str(mode) + if not OCTAL_MODE_REGEX.match(mode): + raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]") + return mode + + +def valid_host_path_propagation(propagation: str): + valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate") + if propagation not in valid_propagations: + raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]") + return propagation + + +def valid_portal_scheme_or_raise(scheme: str): + schemes = ("http", "https") + if scheme not in schemes: + raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]") + return scheme + + +def valid_port_or_raise(port: int): + if port < 1 or port > 65535: + raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535") + return port + + +def valid_ip_or_raise(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + raise RenderError(f"Invalid IP address [{ip}]") + return ip + + +def valid_port_mode_or_raise(mode: str): + modes = ("ingress", "host") + if mode not in modes: + raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]") + return mode + + +def valid_port_protocol_or_raise(protocol: str): + protocols = ("tcp", "udp") + if protocol not in protocols: + raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]") + return protocol + + +def valid_depend_condition_or_raise(condition: str): + valid_conditions = ("service_started", "service_healthy", "service_completed_successfully") + if condition not in valid_conditions: + raise RenderError( + f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]" + ) + return condition + + +def valid_cgroup_perm_or_raise(cgroup_perm: str): + valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "") + if cgroup_perm not in valid_cgroup_perms: + raise RenderError( + f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]" + ) + return cgroup_perm + + +def allowed_dns_opt_or_raise(dns_opt: str): + disallowed_dns_opts = [] + if dns_opt in disallowed_dns_opts: + raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.") + return dns_opt + + +def valid_http_path_or_raise(path: str): + path = _valid_path_or_raise(path) + return path + + +def valid_fs_path_or_raise(path: str): + # There is no reason to allow / as a path, + # either on host or in a container side. + if path == "/": + raise RenderError(f"Path [{path}] cannot be [/]") + path = _valid_path_or_raise(path) + return path + + +def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool: + """ + Validates that the given path (after resolving symlinks) is not + one of the restricted paths or within those restricted directories. + + Returns True if the path is allowed, False otherwise. + """ + # Resolve the path to avoid symlink bypasses + real_path = Path(input_path).resolve() + for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]: + if real_path.is_relative_to(restricted): + return False + + return real_path not in RESTRICTED_IN + + +def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False): + if not is_allowed_path(path, is_ix_volume): + raise RenderError(f"Path [{path}] is not allowed to be mounted.") + return path + + +def _valid_path_or_raise(path: str): + if path == "": + raise RenderError(f"Path [{path}] cannot be empty") + if not path.startswith("/"): + raise RenderError(f"Path [{path}] must start with /") + if "//" in path: + raise RenderError(f"Path [{path}] cannot contain [//]") + return path + + +def allowed_device_or_raise(path: str): + disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"] + if path in disallowed_devices: + raise RenderError(f"Device [{path}] is not allowed to be manually added.") + return path + + +def valid_network_mode_or_raise(mode: str, containers: list[str]): + valid_modes = ("host", "none") + if mode in valid_modes: + return mode + + if mode.startswith("service:"): + if mode[8:] not in containers: + raise RenderError(f"Service [{mode[8:]}] not found") + return mode + + raise RenderError( + f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:]" + ) + + +def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0): + valid_restart_policies = ("always", "on-failure", "unless-stopped", "no") + if policy not in valid_restart_policies: + raise RenderError( + f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]" + ) + if policy != "on-failure" and maximum_retry_count != 0: + raise RenderError("Maximum retry count can only be set for [on-failure] restart policy") + + if maximum_retry_count < 0: + raise RenderError("Maximum retry count must be a positive integer") + + return policy + + +def valid_cap_or_raise(cap: str): + valid_policies = ( + "ALL", + "AUDIT_CONTROL", + "AUDIT_READ", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "BPF", + "CHECKPOINT_RESTORE", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "PERFMON", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "SYSLOG", + "WAKE_ALARM", + ) + + if cap not in valid_policies: + raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]") + + return cap diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_mount.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_mount.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_mount.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_mount.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_mount_types.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_mount_types.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_mount_types.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_mount_types.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_sources.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_sources.py new file mode 100644 index 0000000000..dcfce44b75 --- /dev/null +++ b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_sources.py @@ -0,0 +1,108 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + from storage import IxStorageHostPathConfig, IxStorageIxVolumeConfig, IxStorageVolumeConfig + +try: + from .error import RenderError + from .formatter import get_hashed_name_for_volume + from .validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise +except ImportError: + from error import RenderError + from formatter import get_hashed_name_for_volume + from validations import valid_fs_path_or_raise, allowed_fs_host_path_or_raise + + +class HostPathSource: + def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [host_path_config] to be set for [host_path] type.") + + path = "" + if config.get("acl_enable", False): + acl_path = config.get("acl", {}).get("path") + if not acl_path: + raise RenderError("Expected [host_path_config.acl.path] to be set for [host_path] type.") + path = valid_fs_path_or_raise(acl_path) + else: + path = valid_fs_path_or_raise(config.get("path", "")) + + path = path.rstrip("/") + self.source = allowed_fs_host_path_or_raise(path) + + def get(self): + return self.source + + +class IxVolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageIxVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.") + dataset_name = config.get("dataset_name") + if not dataset_name: + raise RenderError("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type.") + + ix_volumes = self._render_instance.values.get("ix_volumes", {}) + if dataset_name not in ix_volumes: + available = ", ".join(ix_volumes.keys()) + raise RenderError( + f"Expected the key [{dataset_name}] to be set in [ix_volumes] for [ix_volume] type. " + f"Available keys: [{available}]." + ) + + path = valid_fs_path_or_raise(ix_volumes[dataset_name].rstrip("/")) + self.source = allowed_fs_host_path_or_raise(path, True) + + def get(self): + return self.source + + +class CifsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [cifs_config] to be set for [cifs] type.") + self.source = get_hashed_name_for_volume("cifs", config) + + def get(self): + return self.source + + +class NfsSource: + def __init__(self, render_instance: "Render", config: dict): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [nfs_config] to be set for [nfs] type.") + self.source = get_hashed_name_for_volume("nfs", config) + + def get(self): + return self.source + + +class VolumeSource: + def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"): + self._render_instance = render_instance + self.source: str = "" + + if not config: + raise RenderError("Expected [volume_config] to be set for [volume] type.") + + volume_name: str = config.get("volume_name", "") + if not volume_name: + raise RenderError("Expected [volume_config.volume_name] to be set for [volume] type.") + + self.source = volume_name + + def get(self): + return self.source diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_types.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_types.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volume_types.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volume_types.py diff --git a/ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volumes.py b/ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volumes.py similarity index 100% rename from ix-dev/stable/wg-easy/templates/library/base_v2_1_5/volumes.py rename to ix-dev/stable/wg-easy/templates/library/base_v2_1_6/volumes.py