diff --git a/catalog.json b/catalog.json
index d8f0f7d6f6..ab899e2e1a 100644
--- a/catalog.json
+++ b/catalog.json
@@ -13,7 +13,7 @@
"latest_version": "1.2.9",
"latest_app_version": "240915",
"latest_human_version": "240915_1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "photoprism",
"recommended": false,
"title": "Photoprism",
@@ -86,7 +86,7 @@
"latest_version": "1.2.8",
"latest_app_version": "v3.1.0",
"latest_human_version": "v3.1.0_1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "prometheus",
"recommended": false,
"title": "Prometheus",
@@ -134,7 +134,7 @@
"latest_version": "1.2.15",
"latest_app_version": "4.8.10.0",
"latest_human_version": "4.8.10.0_1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "emby",
"recommended": false,
"title": "Emby Server",
@@ -213,7 +213,7 @@
"latest_version": "1.4.9",
"latest_app_version": "2.3.0",
"latest_human_version": "2.3.0_1.4.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "diskoverdata",
"recommended": false,
"title": "Diskover Data",
@@ -293,10 +293,10 @@
"healthy_error": null,
"home": "https://www.collaboraoffice.com/",
"location": "/__w/apps/apps/trains/stable/collabora",
- "latest_version": "1.2.12",
- "latest_app_version": "24.04.12.1.1",
- "latest_human_version": "24.04.12.1.1_1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.2.13",
+ "latest_app_version": "24.04.12.2.1",
+ "latest_human_version": "24.04.12.2.1_1.2.13",
+ "last_update": "2025-01-31 17:33:02",
"name": "collabora",
"recommended": false,
"title": "Collabora",
@@ -385,7 +385,7 @@
"latest_version": "1.4.18",
"latest_app_version": "2025.1.4",
"latest_human_version": "2025.1.4_1.4.18",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "home-assistant",
"recommended": false,
"title": "Home Assistant",
@@ -457,10 +457,10 @@
"healthy_error": null,
"home": "https://www.netdata.cloud/",
"location": "/__w/apps/apps/trains/stable/netdata",
- "latest_version": "1.2.11",
- "latest_app_version": "v2.2.1",
- "latest_human_version": "v2.2.1_1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.2.12",
+ "latest_app_version": "v2.2.2",
+ "latest_human_version": "v2.2.2_1.2.12",
+ "last_update": "2025-01-31 17:33:02",
"name": "netdata",
"recommended": false,
"title": "Netdata",
@@ -537,7 +537,7 @@
"latest_version": "1.2.7",
"latest_app_version": "2024.07.0",
"latest_human_version": "2024.07.0_1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "pihole",
"recommended": false,
"title": "Pi-hole",
@@ -633,7 +633,7 @@
"latest_version": "1.1.11",
"latest_app_version": "1.29.2",
"latest_human_version": "1.29.2_1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "syncthing",
"recommended": false,
"title": "Syncthing",
@@ -718,7 +718,7 @@
"latest_version": "1.2.12",
"latest_app_version": "RELEASE.2025-01-20T14-49-07Z",
"latest_human_version": "RELEASE.2025-01-20T14-49-07Z_1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "minio",
"recommended": false,
"title": "MinIO",
@@ -767,7 +767,7 @@
"latest_version": "1.1.11",
"latest_app_version": "14",
"latest_human_version": "14_1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "wg-easy",
"recommended": false,
"title": "WG Easy",
@@ -824,10 +824,10 @@
"healthy_error": null,
"home": "https://nextcloud.com/",
"location": "/__w/apps/apps/trains/stable/nextcloud",
- "latest_version": "1.5.18",
+ "latest_version": "1.6.0",
"latest_app_version": "30.0.5",
- "latest_human_version": "30.0.5_1.5.18",
- "last_update": "2025-01-30 08:03:00",
+ "latest_human_version": "30.0.5_1.6.0",
+ "last_update": "2025-01-31 17:33:02",
"name": "nextcloud",
"recommended": false,
"title": "Nextcloud",
@@ -942,7 +942,7 @@
"latest_version": "1.1.14",
"latest_app_version": "1.41.3.9314-a0bfb8370",
"latest_human_version": "1.41.3.9314-a0bfb8370_1.1.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "plex",
"recommended": false,
"title": "Plex",
@@ -1020,7 +1020,7 @@
"latest_version": "1.1.9",
"latest_app_version": "1.0.0",
"latest_human_version": "1.0.0_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "ix-app",
"recommended": false,
"title": "iX App",
@@ -1051,7 +1051,7 @@
"latest_version": "1.2.7",
"latest_app_version": "6f87ea801-v1.71.2-go1.18.8",
"latest_human_version": "6f87ea801-v1.71.2-go1.18.8_1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "storj",
"recommended": false,
"title": "Storj",
@@ -1112,7 +1112,7 @@
"latest_version": "1.2.9",
"latest_app_version": "8.17.1",
"latest_human_version": "8.17.1_1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "elastic-search",
"recommended": false,
"title": "Elastic Search",
@@ -1156,10 +1156,10 @@
"healthy_error": null,
"home": "https://hub.docker.com/r/asigra/ds-system",
"location": "/__w/apps/apps/trains/enterprise/asigra-ds-system",
- "latest_version": "1.0.26",
+ "latest_version": "1.0.27",
"latest_app_version": "14.2.0.8",
- "latest_human_version": "14.2.0.8_1.0.26",
- "last_update": "2025-01-30 08:03:00",
+ "latest_human_version": "14.2.0.8_1.0.27",
+ "last_update": "2025-01-31 17:33:02",
"name": "asigra-ds-system",
"recommended": false,
"title": "Asigra DS-System",
@@ -1231,7 +1231,7 @@
"latest_version": "1.1.9",
"latest_app_version": "1.29.2",
"latest_human_version": "1.29.2_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "syncthing",
"recommended": false,
"title": "Syncthing",
@@ -1315,7 +1315,7 @@
"latest_version": "1.2.9",
"latest_app_version": "RELEASE.2024-12-18T13-15-44Z",
"latest_human_version": "RELEASE.2024-12-18T13-15-44Z_1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "minio",
"recommended": false,
"title": "MinIO",
@@ -1364,7 +1364,7 @@
"latest_version": "1.0.1",
"latest_app_version": "v1.76.6",
"latest_human_version": "v1.76.6_1.0.1",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "ix-remote-assist",
"recommended": false,
"title": "Remote Assist",
@@ -1436,7 +1436,7 @@
"latest_version": "1.1.7",
"latest_app_version": "latest",
"latest_human_version": "latest_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "logseq",
"recommended": false,
"title": "Logseq",
@@ -1480,7 +1480,7 @@
"latest_version": "1.2.6",
"latest_app_version": "v4.1.0",
"latest_human_version": "v4.1.0_1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "listmonk",
"recommended": false,
"title": "Listmonk",
@@ -1557,7 +1557,7 @@
"latest_version": "1.1.7",
"latest_app_version": "latest",
"latest_human_version": "latest_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "organizr",
"recommended": false,
"title": "Organizr",
@@ -1626,7 +1626,7 @@
"latest_version": "1.1.12",
"latest_app_version": "1.13.3",
"latest_human_version": "1.13.3_1.1.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "castopod",
"recommended": false,
"title": "Castopod",
@@ -1707,7 +1707,7 @@
"latest_version": "1.2.10",
"latest_app_version": "11.5.0",
"latest_human_version": "11.5.0_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "grafana",
"recommended": false,
"title": "Grafana",
@@ -1758,7 +1758,7 @@
"latest_version": "1.1.9",
"latest_app_version": "2.4.63",
"latest_human_version": "2.4.63_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "webdav",
"recommended": false,
"title": "WebDAV",
@@ -1802,7 +1802,7 @@
"latest_version": "1.1.9",
"latest_app_version": "2.3.0",
"latest_human_version": "2.3.0_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "jellyseerr",
"recommended": false,
"title": "Jellyseerr",
@@ -1848,7 +1848,7 @@
"latest_version": "1.1.15",
"latest_app_version": "v0.10.9",
"latest_human_version": "v0.10.9_1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "homepage",
"recommended": false,
"title": "Homepage",
@@ -1899,7 +1899,7 @@
"latest_version": "1.3.15",
"latest_app_version": "2.26.1",
"latest_human_version": "2.26.1_1.3.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "portainer",
"recommended": false,
"title": "Portainer",
@@ -1981,7 +1981,7 @@
"latest_version": "1.2.15",
"latest_app_version": "v1.57.0",
"latest_human_version": "v1.57.0_1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "autobrr",
"recommended": false,
"title": "Autobrr",
@@ -2030,7 +2030,7 @@
"latest_version": "1.12.10",
"latest_app_version": "2025.1.0",
"latest_human_version": "2025.1.0_1.12.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "minecraft",
"recommended": false,
"title": "Minecraft",
@@ -2096,7 +2096,7 @@
"latest_version": "1.1.9",
"latest_app_version": "0.9.3",
"latest_human_version": "0.9.3_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "whoogle",
"recommended": false,
"title": "Whoogle",
@@ -2144,7 +2144,7 @@
"latest_version": "1.2.10",
"latest_app_version": "1.23.1",
"latest_human_version": "1.23.1_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "gitea",
"recommended": false,
"title": "Gitea",
@@ -2199,10 +2199,10 @@
"healthy_error": null,
"home": "https://n8n.io/",
"location": "/__w/apps/apps/trains/community/n8n",
- "latest_version": "1.5.19",
- "latest_app_version": "1.76.1",
- "latest_human_version": "1.76.1_1.5.19",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.5.20",
+ "latest_app_version": "1.77.0",
+ "latest_human_version": "1.77.0_1.5.20",
+ "last_update": "2025-01-31 17:33:02",
"name": "n8n",
"recommended": false,
"title": "n8n",
@@ -2266,7 +2266,7 @@
"latest_version": "1.4.10",
"latest_app_version": "v2.5.0",
"latest_human_version": "v2.5.0_1.4.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "mealie",
"recommended": false,
"title": "Mealie",
@@ -2319,7 +2319,7 @@
"latest_version": "1.0.9",
"latest_app_version": "2.0.20",
"latest_human_version": "2.0.20_1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "eclipse-mosquitto",
"recommended": false,
"title": "Eclipse Mosquitto",
@@ -2364,7 +2364,7 @@
"latest_version": "1.1.8",
"latest_app_version": "v2.6.4",
"latest_human_version": "v2.6.4_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "sftpgo",
"recommended": false,
"title": "SFTPGo",
@@ -2408,7 +2408,7 @@
"latest_version": "1.0.13",
"latest_app_version": "7.24.0",
"latest_human_version": "7.24.0_1.0.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "calibre",
"recommended": false,
"title": "Calibre",
@@ -2482,7 +2482,7 @@
"latest_version": "1.3.7",
"latest_app_version": "1.25.0",
"latest_human_version": "1.25.0_1.3.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "freshrss",
"recommended": false,
"title": "FreshRSS",
@@ -2550,7 +2550,7 @@
"latest_version": "1.1.9",
"latest_app_version": "2.29.01",
"latest_human_version": "2.29.01_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "tdarr",
"recommended": false,
"title": "Tdarr",
@@ -2616,7 +2616,7 @@
"latest_version": "1.1.7",
"latest_app_version": "0.8.4",
"latest_human_version": "0.8.4_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "kavita",
"recommended": false,
"title": "Kavita",
@@ -2688,7 +2688,7 @@
"latest_version": "1.1.15",
"latest_app_version": "0.4.10.2734",
"latest_human_version": "0.4.10.2734_1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "readarr",
"recommended": false,
"title": "Readarr",
@@ -2738,7 +2738,7 @@
"latest_version": "1.1.8",
"latest_app_version": "15.3.0",
"latest_human_version": "15.3.0_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "piwigo",
"recommended": false,
"title": "Piwigo",
@@ -2817,7 +2817,7 @@
"latest_version": "1.0.14",
"latest_app_version": "5.11.33",
"latest_human_version": "5.11.33_1.0.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "invoice-ninja",
"recommended": false,
"title": "Invoice Ninja",
@@ -2913,7 +2913,7 @@
"latest_version": "1.0.6",
"latest_app_version": "9.1.0",
"latest_human_version": "9.1.0_1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "lyrion-music-server",
"recommended": false,
"title": "Lyrion Music Server",
@@ -2974,7 +2974,7 @@
"latest_version": "1.1.7",
"latest_app_version": "1.33.2",
"latest_human_version": "1.33.2_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "overseerr",
"recommended": false,
"title": "Overseerr",
@@ -3018,7 +3018,7 @@
"latest_version": "1.1.10",
"latest_app_version": "4.0.12.2823",
"latest_human_version": "4.0.12.2823_1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "sonarr",
"recommended": false,
"title": "Sonarr",
@@ -3067,7 +3067,7 @@
"latest_version": "1.2.8",
"latest_app_version": "v1.5.735",
"latest_human_version": "v1.5.735_1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "mumble",
"recommended": false,
"title": "Mumble",
@@ -3111,7 +3111,7 @@
"latest_version": "1.1.9",
"latest_app_version": "1.5.1",
"latest_human_version": "1.5.1_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "bazarr",
"recommended": false,
"title": "Bazarr",
@@ -3146,6 +3146,49 @@
}
]
},
+ "dozzle": {
+ "app_readme": "
Dozzle
Dozzle - Realtime log viewer for docker containers.
",
+ "categories": [
+ "monitoring"
+ ],
+ "description": "Realtime log viewer for docker containers.",
+ "healthy": true,
+ "healthy_error": null,
+ "home": "https://dozzle.dev",
+ "location": "/__w/apps/apps/trains/community/dozzle",
+ "latest_version": "1.0.0",
+ "latest_app_version": "v8.10.5",
+ "latest_human_version": "v8.10.5_1.0.0",
+ "last_update": "2025-01-31 17:33:02",
+ "name": "dozzle",
+ "recommended": false,
+ "title": "Dozzle",
+ "maintainers": [
+ {
+ "email": "dev@ixsystems.com",
+ "name": "truenas",
+ "url": "https://www.truenas.com/"
+ }
+ ],
+ "tags": [
+ "logs"
+ ],
+ "screenshots": [],
+ "sources": [
+ "https://github.com/amir20/dozzle"
+ ],
+ "icon_url": "https://media.sys.truenas.net/apps/dozzle/icons/icon.svg",
+ "capabilities": [],
+ "run_as_context": [
+ {
+ "description": "Dozzle runs as any non-root user.",
+ "gid": 568,
+ "group_name": "dozzle",
+ "uid": 568,
+ "user_name": "dozzle"
+ }
+ ]
+ },
"flaresolverr": {
"app_readme": "FlareSolverr
FlareSolverr - Proxy server to bypass Cloudflare protection
FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.
",
"categories": [
@@ -3159,7 +3202,7 @@
"latest_version": "1.0.16",
"latest_app_version": "v3.3.21",
"latest_human_version": "v3.3.21_1.0.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "flaresolverr",
"recommended": false,
"title": "FlareSolverr",
@@ -3200,7 +3243,7 @@
"latest_version": "1.1.7",
"latest_app_version": "2.0.0",
"latest_human_version": "2.0.0_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "pigallery2",
"recommended": false,
"title": "PiGallery2",
@@ -3256,7 +3299,7 @@
"latest_version": "1.1.8",
"latest_app_version": "0.7.3-nbxyz1",
"latest_human_version": "0.7.3-nbxyz1_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "netbootxyz",
"recommended": false,
"title": "Netboot.xyz",
@@ -3340,7 +3383,7 @@
"latest_version": "1.4.11",
"latest_app_version": "version-6.1.25",
"latest_human_version": "version-6.1.25_1.4.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "firefly-iii",
"recommended": false,
"title": "Firefly III",
@@ -3427,7 +3470,7 @@
"latest_version": "1.0.3",
"latest_app_version": "v2.15.1",
"latest_human_version": "v2.15.1_1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "umami",
"recommended": false,
"title": "Umami",
@@ -3480,7 +3523,7 @@
"latest_version": "1.1.10",
"latest_app_version": "4.0.8",
"latest_human_version": "4.0.8_1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "node-red",
"recommended": false,
"title": "Node-RED",
@@ -3526,7 +3569,7 @@
"latest_version": "1.1.8",
"latest_app_version": "4.0.6",
"latest_human_version": "4.0.6_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "transmission",
"recommended": false,
"title": "Transmission",
@@ -3572,7 +3615,7 @@
"latest_version": "1.2.6",
"latest_app_version": "1.6.9-apache",
"latest_human_version": "1.6.9-apache_1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "roundcube",
"recommended": false,
"title": "Roundcube",
@@ -3651,7 +3694,7 @@
"latest_version": "1.2.10",
"latest_app_version": "1.33.0",
"latest_human_version": "1.33.0_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "vaultwarden",
"recommended": false,
"title": "Vaultwarden",
@@ -3704,7 +3747,7 @@
"latest_version": "1.1.7",
"latest_app_version": "2.10-SNAPSHOT-ocr-es7",
"latest_human_version": "2.10-SNAPSHOT-ocr-es7_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "fscrawler",
"recommended": false,
"title": "FSCrawler",
@@ -3750,7 +3793,7 @@
"latest_version": "1.0.2",
"latest_app_version": "0.2.11",
"latest_human_version": "0.2.11_1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "gitea-act-runner",
"recommended": false,
"title": "Gitea Act Runner",
@@ -3794,10 +3837,10 @@
"healthy_error": null,
"home": "https://github.com/plankanban/planka",
"location": "/__w/apps/apps/trains/community/planka",
- "latest_version": "1.2.6",
- "latest_app_version": "1.24.3",
- "latest_human_version": "1.24.3_1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.2.7",
+ "latest_app_version": "1.24.4",
+ "latest_human_version": "1.24.4_1.2.7",
+ "last_update": "2025-01-31 17:33:02",
"name": "planka",
"recommended": false,
"title": "Planka",
@@ -3849,10 +3892,10 @@
"healthy_error": null,
"home": "https://iconik.io",
"location": "/__w/apps/apps/trains/community/iconik-storage-gateway",
- "latest_version": "1.0.9",
- "latest_app_version": "3.12.0",
- "latest_human_version": "3.12.0_1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.0.10",
+ "latest_app_version": "3.12.1",
+ "latest_human_version": "3.12.1_1.0.10",
+ "last_update": "2025-01-31 17:33:02",
"name": "iconik-storage-gateway",
"recommended": false,
"title": "Iconik Storage Gateway",
@@ -3896,7 +3939,7 @@
"latest_version": "1.0.13",
"latest_app_version": "v0.8.1",
"latest_human_version": "v0.8.1_1.0.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "scrutiny",
"recommended": false,
"title": "Scrutiny",
@@ -3963,7 +4006,7 @@
"latest_version": "1.1.7",
"latest_app_version": "latest",
"latest_human_version": "latest_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "mineos",
"recommended": false,
"title": "MineOS",
@@ -4030,7 +4073,7 @@
"latest_version": "1.3.13",
"latest_app_version": "2.18.1",
"latest_human_version": "2.18.1_1.3.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "audiobookshelf",
"recommended": false,
"title": "Audiobookshelf",
@@ -4081,7 +4124,7 @@
"latest_version": "1.2.6",
"latest_app_version": "17.0",
"latest_human_version": "17.0_1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "odoo",
"recommended": false,
"title": "Odoo",
@@ -4136,7 +4179,7 @@
"latest_version": "1.2.7",
"latest_app_version": "1.37.0",
"latest_human_version": "1.37.0_1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "linkding",
"recommended": false,
"title": "Linkding",
@@ -4189,7 +4232,7 @@
"latest_version": "1.2.8",
"latest_app_version": "2.20241110.0",
"latest_human_version": "2.20241110.0_1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "invidious",
"recommended": false,
"title": "Invidious",
@@ -4244,10 +4287,10 @@
"healthy_error": null,
"home": "https://github.com/Steam-Headless/docker-steam-headless",
"location": "/__w/apps/apps/trains/community/steam-headless",
- "latest_version": "1.0.4",
+ "latest_version": "1.0.5",
"latest_app_version": "debian",
- "latest_human_version": "debian_1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "latest_human_version": "debian_1.0.5",
+ "last_update": "2025-01-31 17:33:02",
"name": "steam-headless",
"recommended": false,
"title": "Steam Headless",
@@ -4340,7 +4383,7 @@
"latest_version": "1.1.16",
"latest_app_version": "10.10.5",
"latest_human_version": "10.10.5_1.1.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "jellyfin",
"recommended": false,
"title": "Jellyfin",
@@ -4392,7 +4435,7 @@
"latest_version": "1.0.3",
"latest_app_version": "2024.11.0",
"latest_human_version": "2024.11.0_1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "minecraft-bedrock",
"recommended": false,
"title": "Minecraft Bedrock",
@@ -4437,7 +4480,7 @@
"latest_version": "1.0.5",
"latest_app_version": "1.17.9",
"latest_human_version": "1.17.9_1.0.5",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "tianji",
"recommended": false,
"title": "Tianji",
@@ -4490,7 +4533,7 @@
"latest_version": "1.1.10",
"latest_app_version": "v2.15.1",
"latest_human_version": "v2.15.1_1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "tautulli",
"recommended": false,
"title": "Tautulli",
@@ -4541,7 +4584,7 @@
"latest_version": "1.0.15",
"latest_app_version": "2.0.0-beta.1",
"latest_human_version": "2.0.0-beta.1_1.0.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "uptime-kuma",
"recommended": false,
"title": "Uptime Kuma",
@@ -4593,7 +4636,7 @@
"latest_version": "1.1.8",
"latest_app_version": "8.14",
"latest_human_version": "8.14_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "pgadmin",
"recommended": false,
"title": "pgAdmin",
@@ -4647,7 +4690,7 @@
"latest_version": "1.1.7",
"latest_app_version": "7.4.0",
"latest_human_version": "7.4.0_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "recyclarr",
"recommended": false,
"title": "Recyclarr",
@@ -4693,7 +4736,7 @@
"latest_version": "1.2.11",
"latest_app_version": "25.1.0",
"latest_human_version": "25.1.0_1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "actual-budget",
"recommended": false,
"title": "Actual Budget",
@@ -4739,10 +4782,10 @@
"healthy_error": null,
"home": "https://ipfs.tech/",
"location": "/__w/apps/apps/trains/community/ipfs",
- "latest_version": "1.1.8",
- "latest_app_version": "v0.32.1",
- "latest_human_version": "v0.32.1_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.1.9",
+ "latest_app_version": "v0.33.0",
+ "latest_human_version": "v0.33.0_1.1.9",
+ "last_update": "2025-01-31 17:33:02",
"name": "ipfs",
"recommended": false,
"title": "IPFS",
@@ -4792,7 +4835,7 @@
"latest_version": "1.0.4",
"latest_app_version": "2024.12.4",
"latest_human_version": "2024.12.4_1.0.4",
- "last_update": "2025-01-30 08:05:27",
+ "last_update": "2025-01-31 17:33:02",
"name": "esphome",
"recommended": false,
"title": "ESPHome",
@@ -4840,7 +4883,7 @@
"latest_version": "1.0.16",
"latest_app_version": "v1.7.9",
"latest_human_version": "v1.7.9_1.0.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "gaseous-server",
"recommended": false,
"title": "Gaseous Server",
@@ -4907,7 +4950,7 @@
"latest_version": "1.1.8",
"latest_app_version": "2.5.0",
"latest_human_version": "2.5.0_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "chia",
"recommended": false,
"title": "Chia",
@@ -4953,7 +4996,7 @@
"latest_version": "1.1.9",
"latest_app_version": "7.4.2",
"latest_human_version": "7.4.2_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "redis",
"recommended": false,
"title": "Redis",
@@ -4998,7 +5041,7 @@
"latest_version": "1.0.6",
"latest_app_version": "2.0.0",
"latest_human_version": "2.0.0_1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "zigbee2mqtt",
"recommended": false,
"title": "Zigbee2MQTT",
@@ -5050,7 +5093,7 @@
"latest_version": "1.2.10",
"latest_app_version": "5.15",
"latest_human_version": "5.15_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "omada-controller",
"recommended": false,
"title": "Omada Controller",
@@ -5118,7 +5161,7 @@
"latest_version": "1.1.7",
"latest_app_version": "3.1.0",
"latest_human_version": "3.1.0_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "dashy",
"recommended": false,
"title": "Dashy",
@@ -5165,7 +5208,7 @@
"latest_version": "1.0.11",
"latest_app_version": "latest",
"latest_human_version": "latest_1.0.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "filestash",
"recommended": false,
"title": "Filestash",
@@ -5214,7 +5257,7 @@
"latest_version": "2.1.8",
"latest_app_version": "v24.12.1",
"latest_human_version": "v24.12.1_2.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "handbrake",
"recommended": false,
"title": "Handbrake",
@@ -5289,7 +5332,7 @@
"latest_version": "1.2.10",
"latest_app_version": "v1.78.3",
"latest_human_version": "v1.78.3_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "tailscale",
"recommended": false,
"title": "Tailscale",
@@ -5359,7 +5402,7 @@
"latest_version": "1.1.16",
"latest_app_version": "5.0.3",
"latest_human_version": "5.0.3_1.1.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "qbittorrent",
"recommended": false,
"title": "qBittorrent",
@@ -5404,10 +5447,10 @@
"healthy_error": null,
"home": "https://www.passbolt.com",
"location": "/__w/apps/apps/trains/community/passbolt",
- "latest_version": "1.1.7",
- "latest_app_version": "4.10.1",
- "latest_human_version": "4.10.1_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.1.8",
+ "latest_app_version": "4.11.0",
+ "latest_human_version": "4.11.0_1.1.8",
+ "last_update": "2025-01-31 17:33:02",
"name": "passbolt",
"recommended": false,
"title": "Passbolt",
@@ -5464,7 +5507,7 @@
"latest_version": "2.1.9",
"latest_app_version": "v24.12.1",
"latest_human_version": "v24.12.1_2.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "homer",
"recommended": false,
"title": "Homer",
@@ -5508,7 +5551,7 @@
"latest_version": "1.1.10",
"latest_app_version": "0.54.4",
"latest_human_version": "0.54.4_1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "navidrome",
"recommended": false,
"title": "Navidrome",
@@ -5556,7 +5599,7 @@
"latest_version": "1.2.19",
"latest_app_version": "2025-01-27",
"latest_human_version": "2025-01-27_1.2.19",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "metube",
"recommended": false,
"title": "MeTube",
@@ -5603,7 +5646,7 @@
"latest_version": "1.1.8",
"latest_app_version": "v1.1.0",
"latest_human_version": "v1.1.0_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "kapowarr",
"recommended": false,
"title": "Kapowarr",
@@ -5651,7 +5694,7 @@
"latest_version": "1.0.15",
"latest_app_version": "17.2",
"latest_human_version": "17.2_1.0.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "postgres",
"recommended": false,
"title": "Postgres",
@@ -5694,7 +5737,7 @@
"latest_version": "1.1.7",
"latest_app_version": "5.4.3",
"latest_human_version": "5.4.3_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "twofactor-auth",
"recommended": false,
"title": "2FAuth",
@@ -5742,7 +5785,7 @@
"latest_version": "1.3.8",
"latest_app_version": "9.0.108",
"latest_human_version": "9.0.108_1.3.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "unifi-controller",
"recommended": false,
"title": "Unifi Controller",
@@ -5789,7 +5832,7 @@
"latest_version": "1.1.9",
"latest_app_version": "2.479.3-jdk17",
"latest_human_version": "2.479.3-jdk17_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "jenkins",
"recommended": false,
"title": "Jenkins",
@@ -5838,7 +5881,7 @@
"latest_version": "1.1.9",
"latest_app_version": "1.0.2",
"latest_human_version": "1.0.2_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "rsyncd",
"recommended": false,
"title": "Rsync Daemon",
@@ -5906,10 +5949,10 @@
"healthy_error": null,
"home": "https://www.tinymediamanager.org/",
"location": "/__w/apps/apps/trains/community/tiny-media-manager",
- "latest_version": "1.1.7",
- "latest_app_version": "5.0.13",
- "latest_human_version": "5.0.13_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.1.8",
+ "latest_app_version": "5.1.1",
+ "latest_human_version": "5.1.1_1.1.8",
+ "last_update": "2025-01-31 17:33:02",
"name": "tiny-media-manager",
"recommended": false,
"title": "Tiny Media Manager",
@@ -5970,10 +6013,10 @@
"healthy_error": null,
"home": "https://github.com/searxng/searxng",
"location": "/__w/apps/apps/trains/community/searxng",
- "latest_version": "1.1.24",
- "latest_app_version": "2025.1.29-fc8938c96",
- "latest_human_version": "2025.1.29-fc8938c96_1.1.24",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.1.25",
+ "latest_app_version": "2025.1.31-eea4d4fd1",
+ "latest_human_version": "2025.1.31-eea4d4fd1_1.1.25",
+ "last_update": "2025-01-31 17:33:02",
"name": "searxng",
"recommended": false,
"title": "SearXNG",
@@ -6026,7 +6069,7 @@
"latest_version": "1.0.9",
"latest_app_version": "0.66.0",
"latest_human_version": "0.66.0_1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "jelu",
"recommended": false,
"title": "Jelu",
@@ -6071,10 +6114,10 @@
"healthy_error": null,
"home": "https://nicolargo.github.io/glances",
"location": "/__w/apps/apps/trains/community/glances",
- "latest_version": "1.0.1",
- "latest_app_version": "4.3.0.7",
- "latest_human_version": "4.3.0.7_1.0.1",
- "last_update": "2025-01-30 08:05:27",
+ "latest_version": "1.0.2",
+ "latest_app_version": "4.3.0.8",
+ "latest_human_version": "4.3.0.8_1.0.2",
+ "last_update": "2025-01-31 17:33:02",
"name": "glances",
"recommended": false,
"title": "Glances",
@@ -6141,7 +6184,7 @@
"latest_version": "1.0.4",
"latest_app_version": "3.7.3",
"latest_human_version": "3.7.3_1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "romm",
"recommended": false,
"title": "Romm",
@@ -6190,7 +6233,7 @@
"latest_version": "1.0.2",
"latest_app_version": "2024.10.22",
"latest_human_version": "2024.10.22_1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "it-tools",
"recommended": false,
"title": "IT Tools",
@@ -6258,7 +6301,7 @@
"latest_version": "1.1.9",
"latest_app_version": "tshock-1.4.4.9-5.2.0-3",
"latest_human_version": "tshock-1.4.4.9-5.2.0-3_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "terraria",
"recommended": false,
"title": "Terraria",
@@ -6306,7 +6349,7 @@
"latest_version": "1.1.9",
"latest_app_version": "1.4.2",
"latest_human_version": "1.4.2_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "dockge",
"recommended": false,
"title": "Dockge",
@@ -6383,10 +6426,10 @@
"healthy_error": null,
"home": "https://docs.paperless-ngx.com",
"location": "/__w/apps/apps/trains/community/paperless-ngx",
- "latest_version": "1.2.19",
+ "latest_version": "1.2.20",
"latest_app_version": "2.14.6",
- "latest_human_version": "2.14.6_1.2.19",
- "last_update": "2025-01-30 08:03:00",
+ "latest_human_version": "2.14.6_1.2.20",
+ "last_update": "2025-01-31 17:33:02",
"name": "paperless-ngx",
"recommended": false,
"title": "Paperless-ngx",
@@ -6486,10 +6529,10 @@
"healthy_error": null,
"home": "https://frigate.video/",
"location": "/__w/apps/apps/trains/community/frigate",
- "latest_version": "1.1.14",
+ "latest_version": "1.1.15",
"latest_app_version": "0.14.1",
- "latest_human_version": "0.14.1_1.1.14",
- "last_update": "2025-01-30 08:03:00",
+ "latest_human_version": "0.14.1_1.1.15",
+ "last_update": "2025-01-31 17:33:02",
"name": "frigate",
"recommended": false,
"title": "Frigate",
@@ -6562,7 +6605,7 @@
"latest_version": "1.2.7",
"latest_app_version": "latest",
"latest_human_version": "latest_1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "briefkasten",
"recommended": false,
"title": "Briefkasten",
@@ -6618,7 +6661,7 @@
"latest_version": "1.3.6",
"latest_app_version": "3.0.1-beta",
"latest_human_version": "3.0.1-beta_1.3.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "joplin",
"recommended": false,
"title": "Joplin",
@@ -6673,7 +6716,7 @@
"latest_version": "1.2.12",
"latest_app_version": "1.19.0",
"latest_human_version": "1.19.0_1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "komga",
"recommended": false,
"title": "Komga",
@@ -6709,6 +6752,72 @@
}
]
},
+ "wyze-bridge": {
+ "app_readme": "Wyze-Bridge
Wyze-Bridge Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras
",
+ "categories": [
+ "security"
+ ],
+ "description": "Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras",
+ "healthy": true,
+ "healthy_error": null,
+ "home": "https://github.com/mrlt8/docker-wyze-bridge",
+ "location": "/__w/apps/apps/trains/community/wyze-bridge",
+ "latest_version": "1.0.0",
+ "latest_app_version": "2.10.3",
+ "latest_human_version": "2.10.3_1.0.0",
+ "last_update": "2025-01-31 17:34:15",
+ "name": "wyze-bridge",
+ "recommended": false,
+ "title": "Wyze Bridge",
+ "maintainers": [
+ {
+ "email": "dev@ixsystems.com",
+ "name": "truenas",
+ "url": "https://www.truenas.com/"
+ }
+ ],
+ "tags": [
+ "camera"
+ ],
+ "screenshots": [
+ "https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png"
+ ],
+ "sources": [
+ "https://github.com/mrlt8/docker-wyze-bridge"
+ ],
+ "icon_url": "https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png",
+ "capabilities": [
+ {
+ "description": "Wyze Bridge is able to change file ownership.",
+ "name": "CHOWN"
+ },
+ {
+ "description": "Wyze Bridge is able to set the setuid attribute on a file.",
+ "name": "SETUID"
+ },
+ {
+ "description": "Wyze Bridge is able to set the setgid attribute on a file.",
+ "name": "SETGID"
+ },
+ {
+ "description": "Wyze Bridge is able to bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.",
+ "name": "FOWNER"
+ },
+ {
+ "description": "Wyze Bridge is able to bypass file read, write, and execute permission checks.",
+ "name": "DAC_OVERRIDE"
+ }
+ ],
+ "run_as_context": [
+ {
+ "description": "Wyze Bridge runs as the root user.",
+ "gid": 0,
+ "group_name": "root",
+ "uid": 0,
+ "user_name": "root"
+ }
+ ]
+ },
"homarr": {
"app_readme": "Homarr
Homarr is a sleek, modern dashboard that puts all of your apps and services at your fingertips.
",
"categories": [
@@ -6722,7 +6831,7 @@
"latest_version": "2.0.7",
"latest_app_version": "v1.3.1",
"latest_human_version": "v1.3.1_2.0.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "homarr",
"recommended": false,
"title": "Homarr",
@@ -6791,7 +6900,7 @@
"latest_version": "1.1.9",
"latest_app_version": "palworld",
"latest_human_version": "palworld_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "palworld",
"recommended": false,
"title": "Palworld",
@@ -6869,7 +6978,7 @@
"latest_version": "1.1.8",
"latest_app_version": "2.1.1",
"latest_human_version": "2.1.1_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "deluge",
"recommended": false,
"title": "Deluge",
@@ -6935,7 +7044,7 @@
"latest_version": "1.1.7",
"latest_app_version": "6.7.1",
"latest_human_version": "6.7.1_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "wordpress",
"recommended": false,
"title": "Wordpress",
@@ -6996,7 +7105,7 @@
"latest_version": "1.1.7",
"latest_app_version": "2.3.1",
"latest_human_version": "2.3.1_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "flame",
"recommended": false,
"title": "Flame",
@@ -7055,7 +7164,7 @@
"latest_version": "1.1.8",
"latest_app_version": "2.8.3",
"latest_human_version": "2.8.3_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "distribution",
"recommended": false,
"title": "Distribution",
@@ -7101,7 +7210,7 @@
"latest_version": "1.2.10",
"latest_app_version": "1.3.2",
"latest_human_version": "1.3.2_1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "plex-auto-languages",
"recommended": false,
"title": "Plex Auto Languages",
@@ -7145,7 +7254,7 @@
"latest_version": "1.3.18",
"latest_app_version": "1.30.2.4939",
"latest_human_version": "1.30.2.4939_1.3.18",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "prowlarr",
"recommended": false,
"title": "Prowlarr",
@@ -7188,10 +7297,10 @@
"healthy_error": null,
"home": "https://github.com/cloudflare/cloudflared",
"location": "/__w/apps/apps/trains/community/cloudflared",
- "latest_version": "1.2.11",
- "latest_app_version": "2025.1.0",
- "latest_human_version": "2025.1.0_1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.2.12",
+ "latest_app_version": "2025.1.1",
+ "latest_human_version": "2025.1.1_1.2.12",
+ "last_update": "2025-01-31 17:33:02",
"name": "cloudflared",
"recommended": false,
"title": "Cloudflared",
@@ -7237,7 +7346,7 @@
"latest_version": "1.2.14",
"latest_app_version": "26.0.9",
"latest_human_version": "26.0.9_1.2.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "drawio",
"recommended": false,
"title": "Draw.io",
@@ -7287,7 +7396,7 @@
"latest_version": "1.2.14",
"latest_app_version": "1.1.2-2",
"latest_human_version": "1.1.2-2_1.2.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "clamav",
"recommended": false,
"title": "ClamAV",
@@ -7353,7 +7462,7 @@
"latest_version": "1.1.7",
"latest_app_version": "1.1.11-1",
"latest_human_version": "1.1.11-1_1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "rust-desk",
"recommended": false,
"title": "Rust Desk",
@@ -7398,7 +7507,7 @@
"latest_version": "2.0.4",
"latest_app_version": "0.6.24",
"latest_human_version": "0.6.24_2.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "calibre-web",
"recommended": false,
"title": "Calibre Web",
@@ -7466,7 +7575,7 @@
"latest_version": "1.0.4",
"latest_app_version": "v1.9.5",
"latest_human_version": "v1.9.5_1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "satisfactory-server",
"recommended": false,
"title": "Satisfactory Server",
@@ -7532,7 +7641,7 @@
"latest_version": "1.1.8",
"latest_app_version": "0.12.0",
"latest_human_version": "0.12.0_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "unifi-protect-backup",
"recommended": false,
"title": "Unifi Protect Backup",
@@ -7594,7 +7703,7 @@
"latest_version": "1.0.2",
"latest_app_version": "0.7.3",
"latest_human_version": "0.7.3_1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "handbrake-web",
"recommended": false,
"title": "Handbrake Web",
@@ -7643,10 +7752,10 @@
"healthy_error": null,
"home": "https://filebrowser.org",
"location": "/__w/apps/apps/trains/community/filebrowser",
- "latest_version": "1.2.7",
- "latest_app_version": "v2.31.2",
- "latest_human_version": "v2.31.2_1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.2.8",
+ "latest_app_version": "v2.32.0",
+ "latest_human_version": "v2.32.0_1.2.8",
+ "last_update": "2025-01-31 17:33:02",
"name": "filebrowser",
"recommended": false,
"title": "File Browser",
@@ -7696,7 +7805,7 @@
"latest_version": "1.1.8",
"latest_app_version": "1.14.2",
"latest_human_version": "1.14.2_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "zerotier",
"recommended": false,
"title": "Zerotier",
@@ -7786,7 +7895,7 @@
"latest_version": "1.2.11",
"latest_app_version": "5.17.2.9580",
"latest_human_version": "5.17.2.9580_1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "radarr",
"recommended": false,
"title": "Radarr",
@@ -7836,7 +7945,7 @@
"latest_version": "1.1.8",
"latest_app_version": "1.0.0",
"latest_human_version": "1.0.0_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "tftpd-hpa",
"recommended": false,
"title": "TFTP Server",
@@ -7898,7 +8007,7 @@
"latest_version": "1.1.15",
"latest_app_version": "v0.107.56",
"latest_human_version": "v0.107.56_1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "adguard-home",
"recommended": false,
"title": "AdGuard Home",
@@ -7968,7 +8077,7 @@
"latest_version": "1.1.11",
"latest_app_version": "2.4.2",
"latest_human_version": "2.4.2_1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "penpot",
"recommended": false,
"title": "Penpot",
@@ -8032,7 +8141,7 @@
"latest_version": "1.0.26",
"latest_app_version": "0.5.7",
"latest_human_version": "0.5.7_1.0.26",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "open-webui",
"recommended": false,
"title": "Open WebUI",
@@ -8080,7 +8189,7 @@
"latest_version": "1.1.9",
"latest_app_version": "4.4.1",
"latest_human_version": "4.4.1_1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "sabnzbd",
"recommended": false,
"title": "SABnzbd",
@@ -8130,7 +8239,7 @@
"latest_version": "1.4.8",
"latest_app_version": "0.24.6",
"latest_human_version": "0.24.6_1.4.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "vikunja",
"recommended": false,
"title": "Vikunja",
@@ -8195,10 +8304,10 @@
"healthy_error": null,
"home": "https://immich.app",
"location": "/__w/apps/apps/trains/community/immich",
- "latest_version": "1.7.24",
- "latest_app_version": "v1.125.6",
- "latest_human_version": "v1.125.6_1.7.24",
- "last_update": "2025-01-30 08:03:00",
+ "latest_version": "1.7.25",
+ "latest_app_version": "v1.125.7",
+ "latest_human_version": "v1.125.7_1.7.25",
+ "last_update": "2025-01-31 17:33:02",
"name": "immich",
"recommended": false,
"title": "Immich",
@@ -8270,7 +8379,7 @@
"latest_version": "1.0.2",
"latest_app_version": "2.5.x",
"latest_human_version": "2.5.x_1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "urbackup",
"recommended": false,
"title": "UrBackup",
@@ -8335,7 +8444,7 @@
"latest_version": "1.1.12",
"latest_app_version": "v2.9.0",
"latest_human_version": "v2.9.0_1.1.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "ddns-updater",
"recommended": false,
"title": "DDNS Updater",
@@ -8383,7 +8492,7 @@
"latest_version": "1.2.15",
"latest_app_version": "2.9.4.4539",
"latest_human_version": "2.9.4.4539_1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "lidarr",
"recommended": false,
"title": "Lidarr",
@@ -8432,7 +8541,7 @@
"latest_version": "1.0.29",
"latest_app_version": "0.5.7",
"latest_human_version": "0.5.7_1.0.29",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "ollama",
"recommended": false,
"title": "Ollama",
@@ -8476,7 +8585,7 @@
"latest_version": "1.1.8",
"latest_app_version": "2.12.2",
"latest_human_version": "2.12.2_1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "nginx-proxy-manager",
"recommended": false,
"title": "Nginx Proxy Manager",
@@ -8549,7 +8658,7 @@
"latest_version": "1.0.3",
"latest_app_version": "latest",
"latest_human_version": "latest_1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "truenas-webui",
"recommended": false,
"title": "TrueNAS WebUI",
@@ -8617,7 +8726,7 @@
"latest_version": "1.0.6",
"latest_app_version": "v1",
"latest_human_version": "v1_1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "nginx",
"recommended": false,
"title": "Nginx",
@@ -8665,7 +8774,7 @@
"latest_version": "1.0.1",
"latest_app_version": "v1",
"latest_human_version": "v1_1.0.1",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "other-nginx",
"recommended": false,
"title": "Other Nginx",
@@ -8713,7 +8822,7 @@
"latest_version": "1.0.2",
"latest_app_version": "v1.76.6",
"latest_human_version": "v1.76.6_1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"name": "ix-remote-assist",
"recommended": false,
"title": "Remote Assist",
diff --git a/cspell.config.yaml b/cspell.config.yaml
index 0e5cb63672..d2085d6839 100644
--- a/cspell.config.yaml
+++ b/cspell.config.yaml
@@ -45,6 +45,7 @@ words:
- domeneshop
- dominio
- dondominio
+ - dozzle
- drawio
- dreamhost
- duckdns
@@ -221,6 +222,7 @@ words:
- rslave
- rstrip
- rsyncd
+ - rtmp
- rtsp
- rtty
- ryshe
@@ -294,7 +296,7 @@ words:
- whiteboarding
- whoogle
- wtfismyip
- - wwwfolder
+ - wyze
- xattr
- zerotier
- zigbee
diff --git a/ix-dev/community/cloudflared/app.yaml b/ix-dev/community/cloudflared/app.yaml
index 8650820375..66623fb86d 100644
--- a/ix-dev/community/cloudflared/app.yaml
+++ b/ix-dev/community/cloudflared/app.yaml
@@ -1,4 +1,4 @@
-app_version: 2025.1.0
+app_version: 2025.1.1
capabilities: []
categories:
- networking
@@ -30,4 +30,4 @@ sources:
- https://hub.docker.com/r/cloudflare/cloudflared
title: Cloudflared
train: community
-version: 1.2.11
+version: 1.2.12
diff --git a/ix-dev/community/cloudflared/ix_values.yaml b/ix-dev/community/cloudflared/ix_values.yaml
index e984fb4d65..aa3a5f471c 100644
--- a/ix-dev/community/cloudflared/ix_values.yaml
+++ b/ix-dev/community/cloudflared/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: cloudflare/cloudflared
- tag: 2025.1.0
+ tag: 2025.1.1
consts:
cloudflared_container_name: cloudflared
diff --git a/ix-dev/community/dozzle/README.md b/ix-dev/community/dozzle/README.md
new file mode 100644
index 0000000000..e11d4ed5f8
--- /dev/null
+++ b/ix-dev/community/dozzle/README.md
@@ -0,0 +1,3 @@
+# Dozzle
+
+[Dozzle](https://dozzle.dev) - Realtime log viewer for docker containers.
diff --git a/ix-dev/community/dozzle/app.yaml b/ix-dev/community/dozzle/app.yaml
new file mode 100644
index 0000000000..83783a28b4
--- /dev/null
+++ b/ix-dev/community/dozzle/app.yaml
@@ -0,0 +1,29 @@
+app_version: v8.10.5
+capabilities: []
+categories:
+- monitoring
+description: Realtime log viewer for docker containers.
+home: https://dozzle.dev
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/dozzle/icons/icon.svg
+keywords:
+- logs
+lib_version: 2.1.14
+lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
+maintainers:
+- email: dev@ixsystems.com
+ name: truenas
+ url: https://www.truenas.com/
+name: dozzle
+run_as_context:
+- description: Dozzle runs as any non-root user.
+ gid: 568
+ group_name: dozzle
+ uid: 568
+ user_name: dozzle
+screenshots: []
+sources:
+- https://github.com/amir20/dozzle
+title: Dozzle
+train: community
+version: 1.0.0
diff --git a/ix-dev/community/dozzle/item.yaml b/ix-dev/community/dozzle/item.yaml
new file mode 100644
index 0000000000..bcd1e0811f
--- /dev/null
+++ b/ix-dev/community/dozzle/item.yaml
@@ -0,0 +1,6 @@
+categories:
+- monitoring
+icon_url: https://media.sys.truenas.net/apps/dozzle/icons/icon.svg
+screenshots: []
+tags:
+- logs
diff --git a/ix-dev/community/dozzle/ix_values.yaml b/ix-dev/community/dozzle/ix_values.yaml
new file mode 100644
index 0000000000..0809938948
--- /dev/null
+++ b/ix-dev/community/dozzle/ix_values.yaml
@@ -0,0 +1,7 @@
+images:
+ image:
+ repository: amir20/dozzle
+ tag: v8.10.5
+
+consts:
+ dozzle_container_name: dozzle
diff --git a/ix-dev/community/dozzle/questions.yaml b/ix-dev/community/dozzle/questions.yaml
new file mode 100644
index 0000000000..501e0403e2
--- /dev/null
+++ b/ix-dev/community/dozzle/questions.yaml
@@ -0,0 +1,339 @@
+groups:
+ - name: Dozzle Configuration
+ description: Configure Dozzle
+ - name: User and Group Configuration
+ description: Configure User and Group for Dozzle
+ - name: Network Configuration
+ description: Configure Network for Dozzle
+ - name: Storage Configuration
+ description: Configure Storage for Dozzle
+ - name: Labels Configuration
+ description: Configure Labels for Dozzle
+ - name: Resources Configuration
+ description: Configure Resources for Dozzle
+
+questions:
+ - variable: TZ
+ group: Dozzle Configuration
+ label: Timezone
+ schema:
+ type: string
+ default: Etc/UTC
+ required: true
+ $ref:
+ - definitions/timezone
+ - variable: dozzle
+ label: ""
+ group: Dozzle Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: additional_envs
+ label: Additional Environment Variables
+ description: Configure additional environment variables for Dozzle.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: env
+ label: Environment Variable
+ schema:
+ type: dict
+ attrs:
+ - variable: name
+ label: Name
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: run_as
+ label: ""
+ group: User and Group Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: user
+ label: User ID
+ description: The user id that Dozzle files will be owned by.
+ schema:
+ type: int
+ min: 568
+ default: 568
+ required: true
+ - variable: group
+ label: Group ID
+ description: The group id that Dozzle files will be owned by.
+ schema:
+ type: int
+ min: 568
+ default: 568
+ required: true
+
+ - variable: network
+ label: ""
+ group: Network Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: web_port
+ label: WebUI Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "!=", ""]]
+ default: 31100
+ required: true
+ $ref:
+ - definitions/port
+ - variable: host_ips
+ label: Host IPs
+ description: IPs on the host to bind this port
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: host_ip
+ label: Host IP
+ schema:
+ type: string
+ required: true
+ $ref:
+ - definitions/node_bind_ip
+ - variable: host_network
+ label: Host Network
+ description: |
+ Bind to the host network. It's recommended to keep this disabled.
+ schema:
+ type: boolean
+ default: false
+ - variable: storage
+ label: ""
+ group: Storage Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: additional_storage
+ label: Additional Storage
+ description: Additional storage for Dozzle.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: storageEntry
+ label: Storage Entry
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ SMB Share: Is a SMB share that is mounted to as a volume.
+ schema:
+ type: string
+ required: true
+ default: "ix_volume"
+ immutable: true
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - value: "cifs"
+ description: SMB/CIFS Share (Mounts a volume to a SMB share)
+ - variable: read_only
+ label: Read Only
+ description: Mount the volume as read only.
+ schema:
+ type: boolean
+ default: false
+ - variable: mount_path
+ label: Mount Path
+ description: The path inside the container to mount the storage.
+ schema:
+ type: path
+ required: true
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "storage_entry"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: cifs_config
+ label: SMB Configuration
+ description: The configuration for the SMB dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "cifs"]]
+ attrs:
+ - variable: server
+ label: Server
+ description: The server to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: path
+ label: Path
+ description: The path to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: username
+ label: Username
+ description: The username to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: password
+ label: Password
+ description: The password to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ private: true
+ - variable: domain
+ label: Domain
+ description: The domain to use for the SMB share.
+ schema:
+ type: string
+ - variable: labels
+ label: ""
+ group: Labels Configuration
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: label
+ label: Label
+ schema:
+ type: dict
+ attrs:
+ - variable: key
+ label: Key
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: containers
+ label: Containers
+ description: Containers where the label should be applied
+ schema:
+ type: list
+ items:
+ - variable: container
+ label: Container
+ schema:
+ type: string
+ required: true
+ enum:
+ - value: dozzle
+ description: dozzle
+ - variable: resources
+ label: ""
+ group: Resources Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: limits
+ label: Limits
+ schema:
+ type: dict
+ attrs:
+ - variable: cpus
+ label: CPUs
+ description: CPUs limit for Dozzle.
+ schema:
+ type: int
+ default: 2
+ required: true
+ - variable: memory
+ label: Memory (in MB)
+ description: Memory limit for Dozzle.
+ schema:
+ type: int
+ default: 4096
+ required: true
diff --git a/ix-dev/community/dozzle/templates/docker-compose.yaml b/ix-dev/community/dozzle/templates/docker-compose.yaml
new file mode 100644
index 0000000000..dfe50e276a
--- /dev/null
+++ b/ix-dev/community/dozzle/templates/docker-compose.yaml
@@ -0,0 +1,22 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.dozzle_container_name, "image") %}
+
+{% do c1.set_user(values.run_as.user, values.run_as.group) %}
+{% do c1.healthcheck.set_custom_test("/dozzle healthcheck") %}
+
+{% do c1.environment.add_env("DOZZLE_ADDR", ":%d"|format(values.network.web_port.port_number)) %}
+{% do c1.environment.add_env("DOZZLE_MODE", "server") %}
+{% do c1.environment.add_user_envs(values.dozzle.additional_envs) %}
+
+{% do c1.add_docker_socket(read_only=True) %}
+
+{% do c1.add_port(values.network.web_port) %}
+
+{% for store in values.storage.additional_storage %}
+ {% do c1.add_storage(store.mount_path, store) %}
+{% endfor %}
+
+{% do tpl.portals.add_portal({"port": values.network.web_port.port_number}) %}
+
+{{ tpl.render() | tojson }}
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/__init__.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/__init__.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/configs.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/configs.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/container.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/container.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/depends.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/depends.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deploy.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deploy.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_mariadb.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_mariadb.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_perms.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_perms.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_postgres.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_postgres.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_redis.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/deps_redis.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/device.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/device.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/device_cgroup_rules.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/devices.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/devices.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/dns.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/dns.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/environment.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/environment.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/error.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/error.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/expose.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/expose.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/extra_hosts.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/extra_hosts.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/formatter.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/formatter.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/functions.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/functions.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/healthcheck.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/healthcheck.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/labels.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/labels.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/notes.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/notes.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/portal.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/portal.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/portals.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/portals.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/ports.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/ports.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/render.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/render.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/resources.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/resources.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/restart.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/restart.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/storage.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/storage.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/sysctls.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/sysctls.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/__init__.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/__init__.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_build_image.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_build_image.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_configs.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_configs.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_container.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_container.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_depends.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_depends.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_deps.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_deps.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_device.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_device.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_dns.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_dns.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_environment.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_environment.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_expose.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_expose.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_formatter.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_formatter.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_functions.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_functions.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_healthcheck.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_labels.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_labels.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_notes.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_notes.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_portal.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_portal.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_ports.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_ports.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_render.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_render.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_resources.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_resources.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_restart.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_restart.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_sysctls.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_validations.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_validations.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_volumes.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/test_volumes.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/validations.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/validations.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_mount.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_mount.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_mount_types.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_mount_types.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_sources.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_sources.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_types.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volume_types.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volumes.py b/ix-dev/community/dozzle/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/volumes.py
rename to ix-dev/community/dozzle/templates/library/base_v2_1_14/volumes.py
diff --git a/ix-dev/community/dozzle/templates/test_values/basic-values.yaml b/ix-dev/community/dozzle/templates/test_values/basic-values.yaml
new file mode 100644
index 0000000000..68c6b99ec0
--- /dev/null
+++ b/ix-dev/community/dozzle/templates/test_values/basic-values.yaml
@@ -0,0 +1,20 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+dozzle:
+ additional_envs: []
+
+network:
+ host_network: false
+ web_port:
+ bind_mode: published
+ port_number: 8080
+
+run_as:
+ user: 568
+ group: 568
+
+storage:
+ additional_storage: []
diff --git a/ix-dev/community/filebrowser/app.yaml b/ix-dev/community/filebrowser/app.yaml
index fd3800f4f5..60e00d9c26 100644
--- a/ix-dev/community/filebrowser/app.yaml
+++ b/ix-dev/community/filebrowser/app.yaml
@@ -1,4 +1,4 @@
-app_version: v2.31.2
+app_version: v2.32.0
capabilities: []
categories:
- storage
@@ -33,4 +33,4 @@ sources:
- https://hub.docker.com/r/filebrowser/filebrowser
title: File Browser
train: community
-version: 1.2.7
+version: 1.2.8
diff --git a/ix-dev/community/filebrowser/ix_values.yaml b/ix-dev/community/filebrowser/ix_values.yaml
index b4127f893c..da02581359 100644
--- a/ix-dev/community/filebrowser/ix_values.yaml
+++ b/ix-dev/community/filebrowser/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: filebrowser/filebrowser
- tag: v2.31.2
+ tag: v2.32.0
consts:
filebrowser_container_name: filebrowser
diff --git a/ix-dev/community/frigate/app.yaml b/ix-dev/community/frigate/app.yaml
index 1cde643063..eaedb8431a 100644
--- a/ix-dev/community/frigate/app.yaml
+++ b/ix-dev/community/frigate/app.yaml
@@ -42,4 +42,4 @@ sources:
- https://github.com/blakeblackshear/frigate
title: Frigate
train: community
-version: 1.1.14
+version: 1.1.15
diff --git a/ix-dev/community/frigate/ix_values.yaml b/ix-dev/community/frigate/ix_values.yaml
index 64dc0b9035..0fb3e2c690 100644
--- a/ix-dev/community/frigate/ix_values.yaml
+++ b/ix-dev/community/frigate/ix_values.yaml
@@ -16,3 +16,5 @@ consts:
notes_body: |
Default credentials are printed in the logs
during the first run of the application.
+ ssl_key_path: /etc/letsencrypt/live/frigate/privkey.pem
+ ssl_cert_path: /etc/letsencrypt/live/frigate/fullchain.pem
diff --git a/ix-dev/community/frigate/questions.yaml b/ix-dev/community/frigate/questions.yaml
index 0e78728192..1d6577ef87 100644
--- a/ix-dev/community/frigate/questions.yaml
+++ b/ix-dev/community/frigate/questions.yaml
@@ -194,6 +194,14 @@ questions:
required: true
$ref:
- definitions/port
+ - variable: certificate_id
+ label: Certificate
+ description: The certificate to use for Frigate.
+ schema:
+ type: int
+ "null": true
+ $ref:
+ - "definitions/certificate"
- variable: storage
label: ""
diff --git a/ix-dev/community/frigate/templates/docker-compose.yaml b/ix-dev/community/frigate/templates/docker-compose.yaml
index d84453f984..899861232e 100644
--- a/ix-dev/community/frigate/templates/docker-compose.yaml
+++ b/ix-dev/community/frigate/templates/docker-compose.yaml
@@ -41,6 +41,12 @@
{% endif %}
{% endif %}
+{% if values.network.certificate_id %}
+ {% set cert = values.ix_certificates[values.network.certificate_id] %}
+ {% do c1.configs.add("private", cert.privatekey, values.consts.ssl_key_path) %}
+ {% do c1.configs.add("public", cert.certificate, values.consts.ssl_cert_path) %}
+{% endif %}
+
{% do tpl.portals.add_portal({"port": values.consts.internal_web_port if values.network.host_network else values.network.web_port, "scheme": "https"}) %}
{% if values.network.enable_no_auth %}
{% do tpl.portals.add_portal({"name": "Web UI (No Auth)", "port": values.consts.internal_no_auth_port if values.network.host_network else values.network.no_auth_port}) %}
diff --git a/ix-dev/community/frigate/templates/test_values/https-values.yaml b/ix-dev/community/frigate/templates/test_values/https-values.yaml
new file mode 100644
index 0000000000..1e9e52fe23
--- /dev/null
+++ b/ix-dev/community/frigate/templates/test_values/https-values.yaml
@@ -0,0 +1,130 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+frigate:
+ image_selector: image
+ shm_size_mb: 64
+ mount_usb_bus: false
+ additional_envs: []
+
+network:
+ host_network: false
+ web_port: 8081
+ enable_no_auth: true
+ no_auth_port: 8080
+ enable_rtsp: true
+ rtsp_port: 8554
+ enable_webrtc: true
+ webrtc_port: 8082
+ enable_go2rtc: true
+ go2rtc_port: 8083
+ certificate_id: "2"
+
+ix_volumes:
+ frigate-config: /opt/tests/mnt/config
+ frigate-media: /opt/tests/mnt/media
+
+storage:
+ config:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: frigate-config
+ create_host_path: true
+ media:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: frigate-media
+ create_host_path: true
+ cache:
+ type: tmpfs
+ tmpfs_config:
+ size: 1024
+ additional_storage: []
+
+ix_certificates:
+ "2":
+ certificate: |
+ -----BEGIN CERTIFICATE-----
+ MIIEdjCCA16gAwIBAgIDYFMYMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+ ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+ VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ HhcNMjEwODMwMjMyMzU0WhcNMjMxMjAzMjMyMzU0WjBuMQswCQYDVQQDDAJhZDEL
+ MAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxDTALBgNVBAcMBGFzZGYxDTALBgNV
+ BAoMBGFkc2YxDTALBgNVBAsMBGFzZGYxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7+1xOHRQyOnQTHFcrdasX
+ Zl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/AbkH7oVFWC1
+ P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI4vQCdYgW
+ 2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2dNgsxKU0H
+ PGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB+Zie331t
+ AzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7A/FuDVg3
+ AgMBAAGjggEdMIIBGTAnBgNVHREEIDAehwTAqAADhwTAqAAFhwTAqAC2hwTAqACB
+ hwTAqACSMB0GA1UdDgQWBBQ4G2ff4tgZl4vmo4xCfqmJhdqShzAMBgNVHRMBAf8E
+ AjAAMIGYBgNVHSMEgZAwgY2AFLlYf9L99nxJDcpCM/LT3V5hQ/a3oXCkbjBsMQww
+ CgYDVQQDDANhc2QxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQH
+ DAJhZjENMAsGA1UECgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkB
+ FgdhQGEuY29tggNgUxcwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/
+ BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBAQA6FpOInEHB5iVk3FP67GybJ29vHZTD
+ KQHbQgmg8s4L7qIsA1HQ+DMCbdylpA11x+t/eL/n48BvGw2FNXpN6uykhLHJjbKR
+ h8yITa2KeD3LjLYhScwIigXmTVYSP3km6s8jRL6UKT9zttnIHyXVpBDya6Q4WTMx
+ fmfC6O7t1PjQ5ZyVtzizIUP8ah9n4TKdXU4A3QIM6WsJXpHb+vqp1WDWJ7mKFtgj
+ x5TKv3wcPnktx0zMPfLb5BTSE9rc9djcBG0eIAsPT4FgiatCUChe7VhuMnqskxEz
+ MymJLoq8+mzucRwFkOkR2EIt1x+Irl2mJVMeBow63rVZfUQBD8h++LqB
+ -----END CERTIFICATE-----
+ -----BEGIN CERTIFICATE-----
+ MIIEhDCCA2ygAwIBAgIDYFMXMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+ ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+ VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ HhcNMjEwODMwMjMyMDQ1WhcNMzEwODI4MjMyMDQ1WjBsMQwwCgYDVQQDDANhc2Qx
+ CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQHDAJhZjENMAsGA1UE
+ CgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkBFgdhQGEuY29tMIIB
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq//c0hEEr83CS1pMgsHX50jt
+ 2MqIbcf63UUNJTiYpUUvUQSFJFc7m/dr+RTZvu97eDCnD5K2qkHHvTPaPZwY+Djf
+ iy7N641Sz6u/y3Yo3xxs1Aermsfedh48vusJpjbkT2XS44VjbkrpKcWDNVpp3Evd
+ M7oJotXeUsZ+imiyVCfr4YhoY5gbGh/r+KN9Wf9YKoUyfLLZGwdZkhtX2zIbidsL
+ Thqi9YTaUHttGinjiBBum234u/CfvKXsfG3yP2gvBGnlvZnM9ktv+lVffYNqlf7H
+ VmB1bKKk84HtzuW5X76SGAgOG8eHX4x5ZLI1WQUuoQOVRl1I0UCjBtbz8XhwvQID
+ AQABo4IBLTCCASkwLQYDVR0RBCYwJIcEwKgABYcEwKgAA4cEwKgAkocEwKgAtYcE
+ wKgAgYcEwKgAtjAdBgNVHQ4EFgQUuVh/0v32fEkNykIz8tPdXmFD9rcwDwYDVR0T
+ AQH/BAUwAwEB/zCBmAYDVR0jBIGQMIGNgBS5WH/S/fZ8SQ3KQjPy091eYUP2t6Fw
+ pG4wbDEMMAoGA1UEAwwDYXNkMQswCQYDVQQGEwJVUzENMAsGA1UECAwEYXNkZjEL
+ MAkGA1UEBwwCYWYxDTALBgNVBAoMBGFzZGYxDDAKBgNVBAsMA2FzZDEWMBQGCSqG
+ SIb3DQEJARYHYUBhLmNvbYIDYFMXMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+ BQcDAjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAKEocOmVuWlr
+ zegtKYMe8NhHIkFY9oVn5ym6RHNOJpPH4QF8XYC3Z5+iC5yGh4P/jVe/4I4SF6Ql
+ PtofU0jNq5vzapt/y+m008eXqPQFmoUOvu+JavoRVcRx2LIP5AgBA1mF56CSREsX
+ TkuJAA9IUQ8EjnmAoAeKINuPaKxGDuU8BGCMqr/qd564MKNf9XYL+Fb2rlkA0O2d
+ 2No34DQLgqSmST/LAvPM7Cbp6knYgnKmGr1nETCXasg1cueHLnWWTvps2HiPp2D/
+ +Fq0uqcZLu4Mdo0CPs4e5sHRyldEnRSKh0DVLprq9zr/GMipmPLJUsT5Jed3sj0w
+ M7Y3vwxshpo=
+ -----END CERTIFICATE-----
+ privatekey: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7+1xOHRQyOnQT
+ HFcrdasXZl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/Abk
+ H7oVFWC1P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI
+ 4vQCdYgW2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2d
+ NgsxKU0HPGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB
+ +Zie331tAzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7
+ A/FuDVg3AgMBAAECggEAapt30rj9DitGTtxAt13pJMEhyYxvvD3WkvmJwguF/Bbu
+ eW0Ba1c668fMeRCA54FWi1sMqusPS4HUqqUvk+tmyAOsAF4qgD/A4MMSC7uJSVI5
+ N/JWhJWyhCY94/FPakiO1nbPbVw41bcqtzU2qvparpME2CtxSCbDiqm7aaag3Kqe
+ EF0fGSUdZ+TYl9JM05+eIyiX+UY19Fg0OjTHMn8nGpxcNTfDBdQ68TKvdo/dtIKL
+ PLKzJUNNdM8odC4CvQtfGMqaslwZwXkiOl5VJcW21ncj/Y0ngEMKeD/i65ZoqGdR
+ 0FKCQYEAGtM2FvJcZQ92Wsw7yj2bK2MSegVUyLK32QKBgQDe8syVCepPzRsfjfxA
+ 6TZlWcGuTZLhwIx97Ktw3VcQ1f4rLoEYlv0xC2VWBORpzIsJo4I/OLmgp8a+Ga8z
+ FkVRnq90dV3t4NP9uJlHgcODHnOardC2UUka4olBSCG6zmK4Jxi34lOxhGRkshOo
+ L4IBeOIB5g+ZrEEXkzfYJHESRQKBgQDX2YhFhGIrT8BAnC5BbXbhm8h6Bhjz8DYL
+ d+qhVJjef7L/aJxViU0hX9Ba2O8CLK3FZeREFE3hJPiJ4TZSlN4evxs5p+bbNDcA
+ 0mhRI/o3X4ac6IxdRebyYnCOB/Cu94/MzppcZcotlCekKNike7eorCcX4Qavm7Pu
+ MUuQ+ifmSwKBgEnchoqZzlbBzMqXb4rRuIO7SL9GU/MWp3TQg7vQmJerTZlgvsQ2
+ wYsOC3SECmhCq4117iCj2luvOdihCboTFsQDnn0mpQe6BIF6Ns3J38wAuqv0CcFd
+ DKsrge1uyD3rQilgSoAhKzkUc24o0PpXQurZ8YZPgbuXpbj5vPaOnCdBAoGACYc7
+ wb3XS4wos3FxhUfcwJbM4b4VKeeHqzfu7pI6cU/3ydiHVitKcVe2bdw3qMPqI9Wc
+ nvi6e17Tbdq4OCsEJx1OiVwFD9YdO3cOTc6lw/3+hjypvZBRYo+/4jUthbu96E+S
+ dtOzehGZMmDvN0uSzupSi3ZOgkAAUFpyuIKickMCgYAId0PCRjonO2thn/R0rZ7P
+ //L852uyzYhXKw5/fjFGhQ6LbaLgIRFaCZ0L2809u0HFnNvJjHv4AKP6j+vFQYYY
+ qQ+66XnfsA9G/bu4MDS9AX83iahD9IdLXQAy8I19prAbpVumKegPbMnNYNB/TYEc
+ 3G15AKCXo7jjOUtHY01DCQ==
+ -----END PRIVATE KEY-----
diff --git a/ix-dev/community/glances/app.yaml b/ix-dev/community/glances/app.yaml
index 57c88f408f..1f0b68f454 100644
--- a/ix-dev/community/glances/app.yaml
+++ b/ix-dev/community/glances/app.yaml
@@ -1,4 +1,4 @@
-app_version: 4.3.0.7
+app_version: 4.3.0.8
capabilities:
- description: Glances is able to bypass permission checks for it's sub-processes.
name: FOWNER
@@ -46,4 +46,4 @@ sources:
- https://hub.docker.com/r/nicolargo/glances
title: Glances
train: community
-version: 1.0.1
+version: 1.0.2
diff --git a/ix-dev/community/glances/ix_values.yaml b/ix-dev/community/glances/ix_values.yaml
index 9d3112e3af..a4018a3a62 100644
--- a/ix-dev/community/glances/ix_values.yaml
+++ b/ix-dev/community/glances/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: nicolargo/glances
- tag: "4.3.0.7"
+ tag: "4.3.0.8"
consts:
glances_container_name: glances
diff --git a/ix-dev/community/iconik-storage-gateway/app.yaml b/ix-dev/community/iconik-storage-gateway/app.yaml
index cf54a8d540..ac9400a1a4 100644
--- a/ix-dev/community/iconik-storage-gateway/app.yaml
+++ b/ix-dev/community/iconik-storage-gateway/app.yaml
@@ -1,4 +1,4 @@
-app_version: 3.12.0
+app_version: 3.12.1
capabilities: []
categories:
- productivity
@@ -28,4 +28,4 @@ sources:
- https://app.iconik.io/help/pages/isg
title: Iconik Storage Gateway
train: community
-version: 1.0.9
+version: 1.0.10
diff --git a/ix-dev/community/iconik-storage-gateway/ix_values.yaml b/ix-dev/community/iconik-storage-gateway/ix_values.yaml
index 0e50978698..978d11987c 100644
--- a/ix-dev/community/iconik-storage-gateway/ix_values.yaml
+++ b/ix-dev/community/iconik-storage-gateway/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ghcr.io/truenas/iconik-storage-gateway-docker
- tag: 3.12.0
+ tag: 3.12.1
consts:
iconik_container_name: iconik
diff --git a/ix-dev/community/immich/app.yaml b/ix-dev/community/immich/app.yaml
index 8771b70c4f..fca2fa7124 100644
--- a/ix-dev/community/immich/app.yaml
+++ b/ix-dev/community/immich/app.yaml
@@ -1,4 +1,4 @@
-app_version: v1.125.6
+app_version: v1.125.7
capabilities:
- description: Immich Proxy is able to chown files.
name: CHOWN
@@ -45,4 +45,4 @@ sources:
- https://github.com/immich-app/immich
title: Immich
train: community
-version: 1.7.24
+version: 1.7.25
diff --git a/ix-dev/community/immich/ix_values.yaml b/ix-dev/community/immich/ix_values.yaml
index 8f09af8cd4..f1cdc2ba6a 100644
--- a/ix-dev/community/immich/ix_values.yaml
+++ b/ix-dev/community/immich/ix_values.yaml
@@ -1,16 +1,16 @@
images:
image:
repository: ghcr.io/immich-app/immich-server
- tag: v1.125.6
+ tag: v1.125.7
ml_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6
+ tag: v1.125.7
ml_cuda_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6-cuda
+ tag: v1.125.7-cuda
ml_openvino_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6-openvino
+ tag: v1.125.7-openvino
pgvecto_image:
repository: tensorchord/pgvecto-rs
tag: pg15-v0.2.0
diff --git a/ix-dev/community/ipfs/app.yaml b/ix-dev/community/ipfs/app.yaml
index 644cf63305..f2eff323b8 100644
--- a/ix-dev/community/ipfs/app.yaml
+++ b/ix-dev/community/ipfs/app.yaml
@@ -1,4 +1,4 @@
-app_version: v0.32.1
+app_version: v0.33.0
capabilities: []
categories:
- storage
@@ -33,4 +33,4 @@ sources:
- https://ipfs.tech/
title: IPFS
train: community
-version: 1.1.8
+version: 1.1.9
diff --git a/ix-dev/community/ipfs/ix_values.yaml b/ix-dev/community/ipfs/ix_values.yaml
index 738a79e13b..906b89b42d 100644
--- a/ix-dev/community/ipfs/ix_values.yaml
+++ b/ix-dev/community/ipfs/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ipfs/kubo
- tag: v0.32.1
+ tag: v0.33.0
consts:
ipfs_container_name: ipfs
diff --git a/ix-dev/community/n8n/app.yaml b/ix-dev/community/n8n/app.yaml
index 0082813c0f..0f1606d594 100644
--- a/ix-dev/community/n8n/app.yaml
+++ b/ix-dev/community/n8n/app.yaml
@@ -1,4 +1,4 @@
-app_version: 1.76.1
+app_version: 1.77.0
capabilities: []
categories:
- productivity
@@ -42,4 +42,4 @@ sources:
- https://hub.docker.com/r/n8nio/n8n
title: n8n
train: community
-version: 1.5.19
+version: 1.5.20
diff --git a/ix-dev/community/n8n/ix_values.yaml b/ix-dev/community/n8n/ix_values.yaml
index 941bfa752b..efa6373876 100644
--- a/ix-dev/community/n8n/ix_values.yaml
+++ b/ix-dev/community/n8n/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: n8nio/n8n
- tag: "1.76.1"
+ tag: "1.77.0"
postgres_15_image:
repository: postgres
tag: "15.10"
diff --git a/ix-dev/community/paperless-ngx/app.yaml b/ix-dev/community/paperless-ngx/app.yaml
index 55c38b5c7a..cf59b5fa4a 100644
--- a/ix-dev/community/paperless-ngx/app.yaml
+++ b/ix-dev/community/paperless-ngx/app.yaml
@@ -67,4 +67,4 @@ sources:
- https://github.com/paperless-ngx/paperless-ngx
title: Paperless-ngx
train: community
-version: 1.2.19
+version: 1.2.20
diff --git a/ix-dev/community/paperless-ngx/ix_values.yaml b/ix-dev/community/paperless-ngx/ix_values.yaml
index 49010b976d..c4e780f742 100644
--- a/ix-dev/community/paperless-ngx/ix_values.yaml
+++ b/ix-dev/community/paperless-ngx/ix_values.yaml
@@ -16,7 +16,7 @@ images:
tag: "3.0.0.0-full"
gotenberg_image:
repository: gotenberg/gotenberg
- tag: "8.15.3"
+ tag: "8.16.0"
consts:
paperless_container_name: paperless
perms_container_name: permissions
diff --git a/ix-dev/community/passbolt/app.yaml b/ix-dev/community/passbolt/app.yaml
index b8b5b8e3b4..3eb21f7c90 100644
--- a/ix-dev/community/passbolt/app.yaml
+++ b/ix-dev/community/passbolt/app.yaml
@@ -1,4 +1,4 @@
-app_version: 4.10.1
+app_version: 4.11.0
capabilities: []
categories:
- security
@@ -37,4 +37,4 @@ sources:
- https://www.passbolt.com
title: Passbolt
train: community
-version: 1.1.7
+version: 1.1.8
diff --git a/ix-dev/community/passbolt/ix_values.yaml b/ix-dev/community/passbolt/ix_values.yaml
index 90cbc810dd..6676c1aee9 100644
--- a/ix-dev/community/passbolt/ix_values.yaml
+++ b/ix-dev/community/passbolt/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: passbolt/passbolt
- tag: 4.10.1-1-ce-non-root
+ tag: 4.11.0-1-ce-non-root
mariadb_image:
repository: mariadb
tag: "10.11.10"
diff --git a/ix-dev/community/planka/app.yaml b/ix-dev/community/planka/app.yaml
index 43b3ab2b05..0d738518f0 100644
--- a/ix-dev/community/planka/app.yaml
+++ b/ix-dev/community/planka/app.yaml
@@ -1,4 +1,4 @@
-app_version: 1.24.3
+app_version: 1.24.4
capabilities: []
categories:
- productivity
@@ -35,4 +35,4 @@ sources:
- https://github.com/plankanban/planka
title: Planka
train: community
-version: 1.2.6
+version: 1.2.7
diff --git a/ix-dev/community/planka/ix_values.yaml b/ix-dev/community/planka/ix_values.yaml
index d115ebdb11..ba82358769 100644
--- a/ix-dev/community/planka/ix_values.yaml
+++ b/ix-dev/community/planka/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ghcr.io/plankanban/planka
- tag: "1.24.3"
+ tag: "1.24.4"
postgres_15_image:
repository: postgres
tag: "15.10"
diff --git a/ix-dev/community/searxng/app.yaml b/ix-dev/community/searxng/app.yaml
index ce4f637981..625ea7b978 100644
--- a/ix-dev/community/searxng/app.yaml
+++ b/ix-dev/community/searxng/app.yaml
@@ -1,4 +1,4 @@
-app_version: 2025.1.29-fc8938c96
+app_version: 2025.1.31-eea4d4fd1
capabilities:
- description: SearXNG requires this ability to switch user for sub-processes.
name: SETUID
@@ -31,4 +31,4 @@ sources:
- https://github.com/searxng/searxng
title: SearXNG
train: community
-version: 1.1.24
+version: 1.1.25
diff --git a/ix-dev/community/searxng/ix_values.yaml b/ix-dev/community/searxng/ix_values.yaml
index 14b1680bb2..041af35e9c 100644
--- a/ix-dev/community/searxng/ix_values.yaml
+++ b/ix-dev/community/searxng/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: searxng/searxng
- tag: 2025.1.29-fc8938c96
+ tag: 2025.1.31-eea4d4fd1
consts:
searxng_container_name: searxng
diff --git a/ix-dev/community/steam-headless/app.yaml b/ix-dev/community/steam-headless/app.yaml
index 0cf59826f2..3ea0963b05 100644
--- a/ix-dev/community/steam-headless/app.yaml
+++ b/ix-dev/community/steam-headless/app.yaml
@@ -58,4 +58,4 @@ sources:
- https://github.com/Steam-Headless/docker-steam-headless
title: Steam Headless
train: community
-version: 1.0.4
+version: 1.0.5
diff --git a/ix-dev/community/steam-headless/questions.yaml b/ix-dev/community/steam-headless/questions.yaml
index 03cfbff838..05b7ba5c18 100644
--- a/ix-dev/community/steam-headless/questions.yaml
+++ b/ix-dev/community/steam-headless/questions.yaml
@@ -754,3 +754,11 @@ questions:
type: int
default: 4096
required: true
+ - variable: gpus
+ group: Resources Configuration
+ label: GPU Configuration
+ schema:
+ type: dict
+ $ref:
+ - "definitions/gpu_configuration"
+ attrs: []
diff --git a/ix-dev/community/tiny-media-manager/app.yaml b/ix-dev/community/tiny-media-manager/app.yaml
index 6381492e65..8048d1841a 100644
--- a/ix-dev/community/tiny-media-manager/app.yaml
+++ b/ix-dev/community/tiny-media-manager/app.yaml
@@ -1,4 +1,4 @@
-app_version: 5.0.13
+app_version: 5.1.1
capabilities:
- description: Tiny Media Manager is able to chown files.
name: CHOWN
@@ -39,4 +39,4 @@ sources:
- https://hub.docker.com/r/tinymediamanager/tinymediamanager
title: Tiny Media Manager
train: community
-version: 1.1.7
+version: 1.1.8
diff --git a/ix-dev/community/tiny-media-manager/ix_values.yaml b/ix-dev/community/tiny-media-manager/ix_values.yaml
index f753c58835..538ddf68a5 100644
--- a/ix-dev/community/tiny-media-manager/ix_values.yaml
+++ b/ix-dev/community/tiny-media-manager/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: tinymediamanager/tinymediamanager
- tag: 5.0.13
+ tag: 5.1.1
consts:
tiny_media_manager_container_name: tiny-media-manager
diff --git a/ix-dev/community/wyze-bridge/README.md b/ix-dev/community/wyze-bridge/README.md
new file mode 100644
index 0000000000..6f693910db
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/README.md
@@ -0,0 +1,3 @@
+# Wyze-Bridge
+
+[Wyze-Bridge](https://github.com/mrlt8/docker-wyze-bridge) Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras
diff --git a/ix-dev/community/wyze-bridge/app.yaml b/ix-dev/community/wyze-bridge/app.yaml
new file mode 100644
index 0000000000..b3462025c8
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/app.yaml
@@ -0,0 +1,43 @@
+app_version: 2.10.3
+capabilities:
+- description: Wyze Bridge is able to change file ownership.
+ name: CHOWN
+- description: Wyze Bridge is able to set the setuid attribute on a file.
+ name: SETUID
+- description: Wyze Bridge is able to set the setgid attribute on a file.
+ name: SETGID
+- description: Wyze Bridge is able to bypass permission checks on operations that
+ normally require the file system UID of the process to match the UID of the file.
+ name: FOWNER
+- description: Wyze Bridge is able to bypass file read, write, and execute permission
+ checks.
+ name: DAC_OVERRIDE
+categories:
+- security
+description: Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for
+ most of your Wyze cameras
+home: https://github.com/mrlt8/docker-wyze-bridge
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png
+keywords:
+- camera
+lib_version: 2.1.14
+lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
+maintainers:
+- email: dev@ixsystems.com
+ name: truenas
+ url: https://www.truenas.com/
+name: wyze-bridge
+run_as_context:
+- description: Wyze Bridge runs as the root user.
+ gid: 0
+ group_name: root
+ uid: 0
+ user_name: root
+screenshots:
+- https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png
+sources:
+- https://github.com/mrlt8/docker-wyze-bridge
+title: Wyze Bridge
+train: community
+version: 1.0.0
diff --git a/ix-dev/community/wyze-bridge/item.yaml b/ix-dev/community/wyze-bridge/item.yaml
new file mode 100644
index 0000000000..69b78f0e9e
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/item.yaml
@@ -0,0 +1,7 @@
+categories:
+- security
+icon_url: https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png
+screenshots:
+- https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png
+tags:
+- camera
diff --git a/ix-dev/community/wyze-bridge/ix_values.yaml b/ix-dev/community/wyze-bridge/ix_values.yaml
new file mode 100644
index 0000000000..2e2de8faf7
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/ix_values.yaml
@@ -0,0 +1,17 @@
+images:
+ image:
+ repository: mrlt8/wyze-bridge
+ tag: 2.10.3
+
+consts:
+ wyze_container_name: wyze-bridge
+ config_container_name: config
+ data_path: /app/data
+ internal_rtmp_port: 1935
+ internal_rtsp_port: 8554
+ internal_hls_port: 8888
+ internal_webrtc_port: 8889
+ internal_webrtc_ice_port: 8189
+ notes_body: |
+ As of May 2024, you will need an API Key and API ID
+ from: https://support.wyze.com/hc/en-us/articles/16129834216731.
diff --git a/ix-dev/community/wyze-bridge/questions.yaml b/ix-dev/community/wyze-bridge/questions.yaml
new file mode 100644
index 0000000000..aa711331a3
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/questions.yaml
@@ -0,0 +1,588 @@
+groups:
+ - name: Wyze Bridge Configuration
+ description: Configure Wyze Bridge
+ - name: Network Configuration
+ description: Configure Network for Wyze Bridge
+ - name: Storage Configuration
+ description: Configure Storage for Wyze Bridge
+ - name: Labels Configuration
+ description: Configure Labels for Wyze Bridge
+ - name: Resources Configuration
+ description: Configure Resources for Wyze Bridge
+
+questions:
+ - variable: TZ
+ group: Wyze Bridge Configuration
+ label: Timezone
+ schema:
+ type: string
+ default: Etc/UTC
+ required: true
+ $ref:
+ - definitions/timezone
+ - variable: wyze
+ label: ""
+ group: Wyze Bridge Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: wb_auth
+ label: Wyze Bridge Authentication Enabled
+ description: Wyze Bridge Authentication Enabled
+ schema:
+ type: boolean
+ default: true
+ required: true
+ - variable: wb_username
+ label: Wyze Bridge Username
+ description: The Wyze Bridge UI username.
+ schema:
+ type: string
+ required: false
+ default: wbadmin
+ show_if: [["wb_auth", "=", true]]
+ - variable: wb_password
+ label: Wyze Bridge Password
+ description: The Wyze Bridge UI password.
+ schema:
+ type: string
+ required: false
+ default: wbadmin
+ show_if: [["wb_auth", "=", true]]
+ - variable: enable_audio
+ label: Enable Camera Audio
+ description: Optional - Enable Audio from Cameras
+ schema:
+ type: boolean
+ default: false
+ - variable: additional_envs
+ label: Additional Environment Variables
+ description: Configure additional environment variables for wyze bridge.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: env
+ label: Environment Variable
+ schema:
+ type: dict
+ attrs:
+ - variable: name
+ label: Name
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: network
+ label: ""
+ group: Network Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: web_port
+ label: WebUI Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "!=", ""]]
+ default: 35000
+ required: true
+ $ref:
+ - definitions/port
+ - variable: rtmp_port
+ label: RTMP Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30100
+ required: true
+ $ref:
+ - definitions/port
+ - variable: rtsp_port
+ label: RTSP Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30101
+ required: true
+ $ref:
+ - definitions/port
+ - variable: hls_port
+ label: HLS Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30102
+ required: true
+ $ref:
+ - definitions/port
+ - variable: enable_webrtc
+ label: Enable WebRTC
+ description: Optional - Enable WebRTC for Wyze Bridge
+ schema:
+ type: boolean
+ default: false
+ - variable: webrtc_ip
+ label: WebRTC IP
+ description: Set this to the host IP to enable a WebRTC stream on port 8889
+ schema:
+ type: ipaddr
+ required: false
+ default: ""
+ show_if: [["enable_webrtc", "=", true]]
+ - variable: webrtc_port
+ label: WebRTC Port
+ schema:
+ type: dict
+ show_if: [["enable_webrtc", "=", true]]
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30103
+ required: true
+ $ref:
+ - definitions/port
+ - variable: webrtc_ice_port
+ label: WebRTC/ICE Port
+ schema:
+ type: dict
+ show_if: [["enable_webrtc", "=", true]]
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30104
+ required: true
+ $ref:
+ - definitions/port
+ - variable: host_network
+ label: Host Network
+ description: |
+ Bind to the host network. It's recommended to keep this disabled.
+ schema:
+ type: boolean
+ default: false
+ - variable: storage
+ label: ""
+ group: Storage Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: data
+ label: wyze bridge Data Storage
+ description: The path to store wyze bridge Data.
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "ix_volume"
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ hidden: true
+ default: "data"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: additional_storage
+ label: Additional Storage
+ description: Additional storage for wyze bridge.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: storageEntry
+ label: Storage Entry
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ SMB Share: Is a SMB share that is mounted to as a volume.
+ schema:
+ type: string
+ required: true
+ default: "ix_volume"
+ immutable: true
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - value: "cifs"
+ description: SMB/CIFS Share (Mounts a volume to a SMB share)
+ - variable: read_only
+ label: Read Only
+ description: Mount the volume as read only.
+ schema:
+ type: boolean
+ default: false
+ - variable: mount_path
+ label: Mount Path
+ description: The path inside the container to mount the storage.
+ schema:
+ type: path
+ required: true
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "storage_entry"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: cifs_config
+ label: SMB Configuration
+ description: The configuration for the SMB dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "cifs"]]
+ attrs:
+ - variable: server
+ label: Server
+ description: The server to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: path
+ label: Path
+ description: The path to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: username
+ label: Username
+ description: The username to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: password
+ label: Password
+ description: The password to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ private: true
+ - variable: domain
+ label: Domain
+ description: The domain to use for the SMB share.
+ schema:
+ type: string
+ - variable: labels
+ label: ""
+ group: Labels Configuration
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: label
+ label: Label
+ schema:
+ type: dict
+ attrs:
+ - variable: key
+ label: Key
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: containers
+ label: Containers
+ description: Containers where the label should be applied
+ schema:
+ type: list
+ items:
+ - variable: container
+ label: Container
+ schema:
+ type: string
+ required: true
+ enum:
+ - value: wyze-bridge
+ description: wyze-bridge
+ - variable: resources
+ label: ""
+ group: Resources Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: limits
+ label: Limits
+ schema:
+ type: dict
+ attrs:
+ - variable: cpus
+ label: CPUs
+ description: CPUs limit for wyze bridge.
+ schema:
+ type: int
+ default: 2
+ required: true
+ - variable: memory
+ label: Memory (in MB)
+ description: Memory limit for wyze bridge.
+ schema:
+ type: int
+ default: 4096
+ required: true
diff --git a/ix-dev/community/wyze-bridge/templates/docker-compose.yaml b/ix-dev/community/wyze-bridge/templates/docker-compose.yaml
new file mode 100644
index 0000000000..08364aef60
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/templates/docker-compose.yaml
@@ -0,0 +1,39 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.wyze_container_name, "image") %}
+{% do c1.add_caps(["CHOWN", "DAC_OVERRIDE", "FOWNER", "KILL", "SETGID", "SETUID"])%}
+
+{% do c1.set_command(["flask", "run", "--host", "0.0.0.0", "--port", values.network.web_port.port_number]) %}
+
+{% do c1.healthcheck.set_test("tcp", {"port": values.network.web_port.port_number}) %}
+
+{% do c1.environment.add_env("ENABLE_AUDIO", values.wyze.enable_audio) %}
+{% do c1.environment.add_env("WB_AUTH", values.wyze.wb_auth) %}
+{% if values.wyze.wb_auth %}
+ {% do c1.environment.add_env("WB_USERNAME", values.wyze.wb_username) %}
+ {% do c1.environment.add_env("WB_PASSWORD", values.wyze.wb_password) %}
+{% endif %}
+
+{% if values.network.enable_webrtc %}
+ {% do c1.environment.add_env("WB_IP", values.network.webrtc_ip) %}
+ {% do c1.add_port(values.network.webrtc_port, {"container_port": values.consts.internal_webrtc_port}) %}
+ {% do c1.add_port(values.network.webrtc_ice_port, {"container_port": values.consts.internal_webrtc_ice_port, "protocol": "udp"}) %}
+{% endif %}
+
+{% do c1.environment.add_user_envs(values.wyze.additional_envs) %}
+
+{% do c1.add_port(values.network.web_port) %}
+{% do c1.add_port(values.network.rtmp_port, {"container_port": values.consts.internal_rtmp_port}) %}
+{% do c1.add_port(values.network.rtsp_port, {"container_port": values.consts.internal_rtsp_port}) %}
+{% do c1.add_port(values.network.hls_port, {"container_port": values.consts.internal_hls_port}) %}
+
+{% do c1.add_storage(values.consts.data_path, values.storage.data) %}
+
+{% for store in values.storage.additional_storage %}
+ {% do c1.add_storage(store.mount_path, store) %}
+{% endfor %}
+
+{% do tpl.portals.add_portal({"port": values.network.web_port.port_number}) %}
+{% do tpl.notes.set_body(values.consts.notes_body) %}
+
+{{ tpl.render() | tojson }}
diff --git a/trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/__init__.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/library/base_v2_1_14/tests/__init__.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/configs.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/configs.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/container.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/container.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/depends.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/depends.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deploy.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deploy.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_mariadb.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_mariadb.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_perms.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_perms.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_postgres.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_postgres.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_redis.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/deps_redis.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/device.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/device.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/device_cgroup_rules.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/devices.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/devices.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/dns.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/dns.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/environment.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/environment.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/error.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/error.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/expose.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/expose.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/extra_hosts.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/extra_hosts.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/formatter.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/formatter.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/functions.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/functions.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/healthcheck.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/healthcheck.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/labels.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/labels.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/notes.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/notes.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/portal.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/portal.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/portals.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/portals.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/ports.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/ports.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/render.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/render.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/resources.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/resources.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/restart.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/restart.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/storage.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/storage.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/sysctls.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/sysctls.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/__init__.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/__init__.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_build_image.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_build_image.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_configs.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_configs.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_container.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_container.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_depends.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_depends.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_deps.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_deps.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_device.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_device.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_dns.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_dns.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_environment.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_environment.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_expose.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_expose.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_formatter.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_formatter.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_functions.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_functions.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_healthcheck.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_labels.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_labels.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_notes.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_notes.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_portal.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_portal.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_ports.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_ports.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_render.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_render.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_resources.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_resources.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_restart.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_restart.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_sysctls.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_validations.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_validations.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_volumes.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/test_volumes.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/validations.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/validations.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_mount.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_mount.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_mount_types.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_mount_types.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_sources.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_sources.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_types.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volume_types.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volumes.py b/ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/volumes.py
rename to ix-dev/community/wyze-bridge/templates/library/base_v2_1_14/volumes.py
diff --git a/ix-dev/community/wyze-bridge/templates/test_values/basic-values.yaml b/ix-dev/community/wyze-bridge/templates/test_values/basic-values.yaml
new file mode 100644
index 0000000000..eabff442c2
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/templates/test_values/basic-values.yaml
@@ -0,0 +1,38 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: true
+ wb_username: wb-admin
+ wb_password: wb-password
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/ix-dev/community/wyze-bridge/templates/test_values/noauth-values.yaml b/ix-dev/community/wyze-bridge/templates/test_values/noauth-values.yaml
new file mode 100644
index 0000000000..a2c5d272f9
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/templates/test_values/noauth-values.yaml
@@ -0,0 +1,37 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: false
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ host_ip: 0.0.0.0
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/ix-dev/community/wyze-bridge/templates/test_values/webrtc-values.yaml b/ix-dev/community/wyze-bridge/templates/test_values/webrtc-values.yaml
new file mode 100644
index 0000000000..495ee172f3
--- /dev/null
+++ b/ix-dev/community/wyze-bridge/templates/test_values/webrtc-values.yaml
@@ -0,0 +1,46 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: true
+ wb_username: wb-admin
+ wb_password: wb-password
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+ enable_webrtc: true
+ webrtc_ip: 127.0.0.1
+ webrtc_port:
+ bind_mode: published
+ port_number: 8889
+ webrtc_ice_port:
+ bind_mode: published
+ port_number: 8189
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/ix-dev/enterprise/asigra-ds-system/app.yaml b/ix-dev/enterprise/asigra-ds-system/app.yaml
index 0d9db1c075..1c8489c68e 100644
--- a/ix-dev/enterprise/asigra-ds-system/app.yaml
+++ b/ix-dev/enterprise/asigra-ds-system/app.yaml
@@ -47,4 +47,4 @@ sources:
- https://hub.docker.com/r/asigra/ds-system
title: Asigra DS-System
train: enterprise
-version: 1.0.26
+version: 1.0.27
diff --git a/ix-dev/enterprise/asigra-ds-system/ix_values.yaml b/ix-dev/enterprise/asigra-ds-system/ix_values.yaml
index 1256c77a81..cff21d9cce 100644
--- a/ix-dev/enterprise/asigra-ds-system/ix_values.yaml
+++ b/ix-dev/enterprise/asigra-ds-system/ix_values.yaml
@@ -7,7 +7,7 @@ images:
tag: 16.6
haproxy_image:
repository: haproxy
- tag: 3.1.2
+ tag: 3.1.3
consts:
asigra_container_name: dssystem
diff --git a/ix-dev/stable/collabora/app.yaml b/ix-dev/stable/collabora/app.yaml
index 08e18166d7..b9c0c4af2c 100644
--- a/ix-dev/stable/collabora/app.yaml
+++ b/ix-dev/stable/collabora/app.yaml
@@ -1,4 +1,4 @@
-app_version: 24.04.12.1.1
+app_version: 24.04.12.2.1
capabilities:
- description: Collabora and Nginx are able to chown files.
name: CHOWN
@@ -53,4 +53,4 @@ sources:
- https://hub.docker.com/r/collabora/code
title: Collabora
train: stable
-version: 1.2.12
+version: 1.2.13
diff --git a/ix-dev/stable/collabora/ix_values.yaml b/ix-dev/stable/collabora/ix_values.yaml
index 5abfc2174e..671e1b8b61 100644
--- a/ix-dev/stable/collabora/ix_values.yaml
+++ b/ix-dev/stable/collabora/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: collabora/code
- tag: 24.04.12.1.1
+ tag: 24.04.12.2.1
nginx_image:
repository: nginx
tag: 1.27.3
diff --git a/ix-dev/stable/netdata/app.yaml b/ix-dev/stable/netdata/app.yaml
index cf2f91d7cf..a7a9b9708b 100644
--- a/ix-dev/stable/netdata/app.yaml
+++ b/ix-dev/stable/netdata/app.yaml
@@ -1,4 +1,4 @@
-app_version: v2.2.1
+app_version: v2.2.2
capabilities:
- description: Netdata is able to chown files.
name: CHOWN
@@ -58,4 +58,4 @@ sources:
- https://github.com/netdata/netdata
title: Netdata
train: stable
-version: 1.2.11
+version: 1.2.12
diff --git a/ix-dev/stable/netdata/ix_values.yaml b/ix-dev/stable/netdata/ix_values.yaml
index 7bccc35c58..9c846ebe35 100644
--- a/ix-dev/stable/netdata/ix_values.yaml
+++ b/ix-dev/stable/netdata/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: netdata/netdata
- tag: v2.2.1
+ tag: v2.2.2
consts:
netdata_container_name: netdata
diff --git a/ix-dev/stable/nextcloud/app.yaml b/ix-dev/stable/nextcloud/app.yaml
index c90a90a205..22a8472c0a 100644
--- a/ix-dev/stable/nextcloud/app.yaml
+++ b/ix-dev/stable/nextcloud/app.yaml
@@ -73,4 +73,4 @@ sources:
- https://github.com/truenas/charts/tree/master/charts/nextcloud
title: Nextcloud
train: stable
-version: 1.5.18
+version: 1.6.0
diff --git a/ix-dev/stable/nextcloud/questions.yaml b/ix-dev/stable/nextcloud/questions.yaml
index 9ae5261d9a..0989f000ef 100644
--- a/ix-dev/stable/nextcloud/questions.yaml
+++ b/ix-dev/stable/nextcloud/questions.yaml
@@ -310,6 +310,18 @@ questions:
max: 65535
show_if: [["use_different_port", "=", true]]
required: true
+ - variable: custom_confs
+ label: Custom Nginx Configurations
+ description: List of custom Nginx configurations.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: conf
+ label: Configuration
+ schema:
+ type: hostpath
+ required: true
- variable: storage
label: ""
diff --git a/ix-dev/stable/nextcloud/templates/docker-compose.yaml b/ix-dev/stable/nextcloud/templates/docker-compose.yaml
index 63b505d07b..8d4bab0490 100644
--- a/ix-dev/stable/nextcloud/templates/docker-compose.yaml
+++ b/ix-dev/stable/nextcloud/templates/docker-compose.yaml
@@ -1,5 +1,5 @@
{% from "macros/nc.jinja.sh" import occ, hosts_update, trusted_domains_update, imaginary_url %}
-{% from "macros/nc.jinja.conf" import opcache, php, limit_request_body, nginx_conf %}
+{% from "macros/nc.jinja.conf" import opcache, php, limit_request_body, use_x_real_ip_in_logs, nginx_conf %}
{% set tpl = ix_lib.base.render.Render(values) %}
@@ -116,6 +116,7 @@
{% do nc_env.x.append(("APACHE_DISABLE_REWRITE_IP", 1)) %}
{% do nc_env.x.append(("OVERWRITEPROTOCOL", "https")) %}
{% do nc_env.x.append(("TRUSTED_PROXIES", ["127.0.0.1", "192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8"] | join(" "))) %}
+ {% do nc_confs.append(("logformat.conf", use_x_real_ip_in_logs(), "/etc/apache2/conf-enabled/logformat.conf", "")) %}
{% if values.nextcloud.host and values.network.nginx.use_different_port %}
{% set host.x = "%s:%d"|format(values.nextcloud.host, values.network.nginx.external_port) %}
{% do nc_env.x.append(("OVERWRITEHOST", host.x)) %}
@@ -199,6 +200,9 @@
{% do nginx_container.configs.add("private", values.ix_certificates[values.network.certificate_id].privatekey, values.consts.ssl_key_path) %}
{% do nginx_container.configs.add("public", values.ix_certificates[values.network.certificate_id].certificate, values.consts.ssl_cert_path) %}
{% do nginx_container.configs.add("nginx.conf", nginx_conf(values), "/etc/nginx/nginx.conf", "0600") %}
+ {% for conf_path in values.network.nginx.custom_confs %}
+ {% do nginx_container.add_storage("/etc/nginx/includes/%d.conf"|format(loop.index0), {"type": "host_path", "host_path_config": {"path": conf_path}}) %}
+ {% endfor %}
{% do nginx_container.add_storage("/tmp", {"type": "anonymous", "volume_config": {}}) %}
{% do nginx_container.healthcheck.set_test("curl", {
"port": values.network.web_port, "path": "/status.php",
diff --git a/ix-dev/stable/nextcloud/templates/macros/nc.jinja.conf b/ix-dev/stable/nextcloud/templates/macros/nc.jinja.conf
index 6bd1f670a8..7da03a164a 100644
--- a/ix-dev/stable/nextcloud/templates/macros/nc.jinja.conf
+++ b/ix-dev/stable/nextcloud/templates/macros/nc.jinja.conf
@@ -11,6 +11,13 @@ max_execution_time={{ values.nextcloud.max_execution_time }}
LimitRequestBody {{ values.nextcloud.php_upload_limit * bytes_gb }}
{%- endmacro -%}
+{% macro use_x_real_ip_in_logs() -%}
+{# `(%{X-Real-IP}i)` is added after each LogFormat `%h` statement from /etc/apache2/apache2.conf -#}
+LogFormat "%v:%p %h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+LogFormat "%h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+LogFormat "%h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O" common
+{%- endmacro -%}
+
{% macro nginx_conf(values) -%}
{%- set port = namespace(x=":$server_port") -%}
{%- if values.network.nginx.use_different_port -%}
@@ -35,6 +42,7 @@ http {
client_max_body_size {{ values.nextcloud.php_upload_limit }}G;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload" always;
+
location = /robots.txt {
allow all;
log_not_found off;
@@ -70,6 +78,8 @@ http {
proxy_send_timeout {{ values.network.nginx.proxy_timeout }}s;
proxy_read_timeout {{ values.network.nginx.proxy_timeout }}s;
}
+
+ include /etc/nginx/includes/*.conf;
}
}
{%- endmacro -%}
diff --git a/trains/community/actual-budget/app_versions.json b/trains/community/actual-budget/app_versions.json
index c57480088d..e5af021bcc 100644
--- a/trains/community/actual-budget/app_versions.json
+++ b/trains/community/actual-budget/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/actual-budget/1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "25.1.0_1.2.11",
"version": "1.2.11",
diff --git a/trains/community/adguard-home/app_versions.json b/trains/community/adguard-home/app_versions.json
index cd4058dc99..fb99d8748a 100644
--- a/trains/community/adguard-home/app_versions.json
+++ b/trains/community/adguard-home/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/adguard-home/1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v0.107.56_1.1.15",
"version": "1.1.15",
diff --git a/trains/community/audiobookshelf/app_versions.json b/trains/community/audiobookshelf/app_versions.json
index f39dab3d13..87b459a7f2 100644
--- a/trains/community/audiobookshelf/app_versions.json
+++ b/trains/community/audiobookshelf/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/audiobookshelf/1.3.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.18.1_1.3.13",
"version": "1.3.13",
diff --git a/trains/community/autobrr/app_versions.json b/trains/community/autobrr/app_versions.json
index d545c23840..c2a360d0d4 100644
--- a/trains/community/autobrr/app_versions.json
+++ b/trains/community/autobrr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/autobrr/1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.57.0_1.2.15",
"version": "1.2.15",
diff --git a/trains/community/bazarr/app_versions.json b/trains/community/bazarr/app_versions.json
index 08d64d5036..8f96c8b559 100644
--- a/trains/community/bazarr/app_versions.json
+++ b/trains/community/bazarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/bazarr/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.5.1_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/briefkasten/app_versions.json b/trains/community/briefkasten/app_versions.json
index 6270d4b514..d30bca3395 100644
--- a/trains/community/briefkasten/app_versions.json
+++ b/trains/community/briefkasten/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/briefkasten/1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.2.7",
"version": "1.2.7",
diff --git a/trains/community/calibre-web/app_versions.json b/trains/community/calibre-web/app_versions.json
index 38164be522..0bc5f7ff9c 100644
--- a/trains/community/calibre-web/app_versions.json
+++ b/trains/community/calibre-web/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/calibre-web/2.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.6.24_2.0.4",
"version": "2.0.4",
diff --git a/trains/community/calibre/app_versions.json b/trains/community/calibre/app_versions.json
index 5fc662b642..2c484f12b3 100644
--- a/trains/community/calibre/app_versions.json
+++ b/trains/community/calibre/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/calibre/1.0.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "7.24.0_1.0.13",
"version": "1.0.13",
diff --git a/trains/community/castopod/app_versions.json b/trains/community/castopod/app_versions.json
index b0a7a84d26..f82b56d86f 100644
--- a/trains/community/castopod/app_versions.json
+++ b/trains/community/castopod/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/castopod/1.1.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.13.3_1.1.12",
"version": "1.1.12",
diff --git a/trains/community/chia/app_versions.json b/trains/community/chia/app_versions.json
index 1c67fcbb1e..8e64434a64 100644
--- a/trains/community/chia/app_versions.json
+++ b/trains/community/chia/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/chia/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.5.0_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/clamav/app_versions.json b/trains/community/clamav/app_versions.json
index 4a352d41e0..eb59ba481f 100644
--- a/trains/community/clamav/app_versions.json
+++ b/trains/community/clamav/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/clamav/1.2.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.1.2-2_1.2.14",
"version": "1.2.14",
diff --git a/trains/community/cloudflared/1.2.11/README.md b/trains/community/cloudflared/1.2.12/README.md
similarity index 100%
rename from trains/community/cloudflared/1.2.11/README.md
rename to trains/community/cloudflared/1.2.12/README.md
diff --git a/trains/community/cloudflared/1.2.11/app.yaml b/trains/community/cloudflared/1.2.12/app.yaml
similarity index 95%
rename from trains/community/cloudflared/1.2.11/app.yaml
rename to trains/community/cloudflared/1.2.12/app.yaml
index 8650820375..66623fb86d 100644
--- a/trains/community/cloudflared/1.2.11/app.yaml
+++ b/trains/community/cloudflared/1.2.12/app.yaml
@@ -1,4 +1,4 @@
-app_version: 2025.1.0
+app_version: 2025.1.1
capabilities: []
categories:
- networking
@@ -30,4 +30,4 @@ sources:
- https://hub.docker.com/r/cloudflare/cloudflared
title: Cloudflared
train: community
-version: 1.2.11
+version: 1.2.12
diff --git a/trains/community/cloudflared/1.2.11/ix_values.yaml b/trains/community/cloudflared/1.2.12/ix_values.yaml
similarity index 88%
rename from trains/community/cloudflared/1.2.11/ix_values.yaml
rename to trains/community/cloudflared/1.2.12/ix_values.yaml
index e984fb4d65..aa3a5f471c 100644
--- a/trains/community/cloudflared/1.2.11/ix_values.yaml
+++ b/trains/community/cloudflared/1.2.12/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: cloudflare/cloudflared
- tag: 2025.1.0
+ tag: 2025.1.1
consts:
cloudflared_container_name: cloudflared
diff --git a/trains/community/cloudflared/1.2.11/migrations/migrate_from_kubernetes b/trains/community/cloudflared/1.2.12/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migrate_from_kubernetes
rename to trains/community/cloudflared/1.2.12/migrations/migrate_from_kubernetes
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/__init__.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/__init__.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/__init__.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/cpu.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/cpu.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/cpu.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/dns_config.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/dns_config.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/memory.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/memory.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/memory.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/resources.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/resources.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/resources.py
diff --git a/trains/community/cloudflared/1.2.11/migrations/migration_helpers/storage.py b/trains/community/cloudflared/1.2.12/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/cloudflared/1.2.11/migrations/migration_helpers/storage.py
rename to trains/community/cloudflared/1.2.12/migrations/migration_helpers/storage.py
diff --git a/trains/community/cloudflared/1.2.11/questions.yaml b/trains/community/cloudflared/1.2.12/questions.yaml
similarity index 100%
rename from trains/community/cloudflared/1.2.11/questions.yaml
rename to trains/community/cloudflared/1.2.12/questions.yaml
diff --git a/trains/community/cloudflared/1.2.11/templates/docker-compose.yaml b/trains/community/cloudflared/1.2.12/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/docker-compose.yaml
rename to trains/community/cloudflared/1.2.12/templates/docker-compose.yaml
diff --git a/trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/configs.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/configs.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/container.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/container.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/depends.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/depends.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deploy.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deploy.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_perms.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_redis.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/device.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/device.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/devices.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/devices.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/dns.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/dns.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/environment.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/environment.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/error.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/error.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/expose.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/expose.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/formatter.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/formatter.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/functions.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/functions.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/healthcheck.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/labels.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/labels.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/notes.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/notes.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/portal.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/portal.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/portals.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/portals.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/ports.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/ports.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/render.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/render.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/resources.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/resources.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/restart.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/restart.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/storage.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/storage.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/sysctls.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/__init__.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/__init__.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/validations.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/validations.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_mount.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_sources.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_types.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volumes.py b/trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/volumes.py
rename to trains/community/cloudflared/1.2.12/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/cloudflared/1.2.11/templates/test_values/basic-values.yaml b/trains/community/cloudflared/1.2.12/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/cloudflared/1.2.11/templates/test_values/basic-values.yaml
rename to trains/community/cloudflared/1.2.12/templates/test_values/basic-values.yaml
diff --git a/trains/community/cloudflared/app_versions.json b/trains/community/cloudflared/app_versions.json
index 2b70bece67..dce54072e5 100644
--- a/trains/community/cloudflared/app_versions.json
+++ b/trains/community/cloudflared/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.2.11": {
+ "1.2.12": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/cloudflared/1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/cloudflared/1.2.12",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "2025.1.0_1.2.11",
- "version": "1.2.11",
+ "human_version": "2025.1.1_1.2.12",
+ "version": "1.2.12",
"app_metadata": {
- "app_version": "2025.1.0",
+ "app_version": "2025.1.1",
"capabilities": [],
"categories": [
"networking"
@@ -49,7 +49,7 @@
],
"title": "Cloudflared",
"train": "community",
- "version": "1.2.11"
+ "version": "1.2.12"
},
"schema": {
"groups": [
diff --git a/trains/community/dashy/app_versions.json b/trains/community/dashy/app_versions.json
index c2bbee85eb..6e0b61c54e 100644
--- a/trains/community/dashy/app_versions.json
+++ b/trains/community/dashy/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/dashy/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "3.1.0_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/ddns-updater/app_versions.json b/trains/community/ddns-updater/app_versions.json
index 465f98a1eb..a090c618dd 100644
--- a/trains/community/ddns-updater/app_versions.json
+++ b/trains/community/ddns-updater/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/ddns-updater/1.1.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v2.9.0_1.1.12",
"version": "1.1.12",
diff --git a/trains/community/deluge/app_versions.json b/trains/community/deluge/app_versions.json
index b09b48825f..76fe2ebfea 100644
--- a/trains/community/deluge/app_versions.json
+++ b/trains/community/deluge/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/deluge/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.1.1_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/distribution/app_versions.json b/trains/community/distribution/app_versions.json
index 785dde0110..34c182fb4d 100644
--- a/trains/community/distribution/app_versions.json
+++ b/trains/community/distribution/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/distribution/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.8.3_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/dockge/app_versions.json b/trains/community/dockge/app_versions.json
index d04461dde9..a4cd527516 100644
--- a/trains/community/dockge/app_versions.json
+++ b/trains/community/dockge/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/dockge/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.4.2_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/dozzle/1.0.0/README.md b/trains/community/dozzle/1.0.0/README.md
new file mode 100644
index 0000000000..e11d4ed5f8
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/README.md
@@ -0,0 +1,3 @@
+# Dozzle
+
+[Dozzle](https://dozzle.dev) - Realtime log viewer for docker containers.
diff --git a/trains/community/dozzle/1.0.0/app.yaml b/trains/community/dozzle/1.0.0/app.yaml
new file mode 100644
index 0000000000..83783a28b4
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/app.yaml
@@ -0,0 +1,29 @@
+app_version: v8.10.5
+capabilities: []
+categories:
+- monitoring
+description: Realtime log viewer for docker containers.
+home: https://dozzle.dev
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/dozzle/icons/icon.svg
+keywords:
+- logs
+lib_version: 2.1.14
+lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
+maintainers:
+- email: dev@ixsystems.com
+ name: truenas
+ url: https://www.truenas.com/
+name: dozzle
+run_as_context:
+- description: Dozzle runs as any non-root user.
+ gid: 568
+ group_name: dozzle
+ uid: 568
+ user_name: dozzle
+screenshots: []
+sources:
+- https://github.com/amir20/dozzle
+title: Dozzle
+train: community
+version: 1.0.0
diff --git a/trains/community/dozzle/1.0.0/ix_values.yaml b/trains/community/dozzle/1.0.0/ix_values.yaml
new file mode 100644
index 0000000000..0809938948
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/ix_values.yaml
@@ -0,0 +1,7 @@
+images:
+ image:
+ repository: amir20/dozzle
+ tag: v8.10.5
+
+consts:
+ dozzle_container_name: dozzle
diff --git a/trains/community/dozzle/1.0.0/questions.yaml b/trains/community/dozzle/1.0.0/questions.yaml
new file mode 100644
index 0000000000..501e0403e2
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/questions.yaml
@@ -0,0 +1,339 @@
+groups:
+ - name: Dozzle Configuration
+ description: Configure Dozzle
+ - name: User and Group Configuration
+ description: Configure User and Group for Dozzle
+ - name: Network Configuration
+ description: Configure Network for Dozzle
+ - name: Storage Configuration
+ description: Configure Storage for Dozzle
+ - name: Labels Configuration
+ description: Configure Labels for Dozzle
+ - name: Resources Configuration
+ description: Configure Resources for Dozzle
+
+questions:
+ - variable: TZ
+ group: Dozzle Configuration
+ label: Timezone
+ schema:
+ type: string
+ default: Etc/UTC
+ required: true
+ $ref:
+ - definitions/timezone
+ - variable: dozzle
+ label: ""
+ group: Dozzle Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: additional_envs
+ label: Additional Environment Variables
+ description: Configure additional environment variables for Dozzle.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: env
+ label: Environment Variable
+ schema:
+ type: dict
+ attrs:
+ - variable: name
+ label: Name
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: run_as
+ label: ""
+ group: User and Group Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: user
+ label: User ID
+ description: The user id that Dozzle files will be owned by.
+ schema:
+ type: int
+ min: 568
+ default: 568
+ required: true
+ - variable: group
+ label: Group ID
+ description: The group id that Dozzle files will be owned by.
+ schema:
+ type: int
+ min: 568
+ default: 568
+ required: true
+
+ - variable: network
+ label: ""
+ group: Network Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: web_port
+ label: WebUI Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "!=", ""]]
+ default: 31100
+ required: true
+ $ref:
+ - definitions/port
+ - variable: host_ips
+ label: Host IPs
+ description: IPs on the host to bind this port
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: host_ip
+ label: Host IP
+ schema:
+ type: string
+ required: true
+ $ref:
+ - definitions/node_bind_ip
+ - variable: host_network
+ label: Host Network
+ description: |
+ Bind to the host network. It's recommended to keep this disabled.
+ schema:
+ type: boolean
+ default: false
+ - variable: storage
+ label: ""
+ group: Storage Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: additional_storage
+ label: Additional Storage
+ description: Additional storage for Dozzle.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: storageEntry
+ label: Storage Entry
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ SMB Share: Is a SMB share that is mounted to as a volume.
+ schema:
+ type: string
+ required: true
+ default: "ix_volume"
+ immutable: true
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - value: "cifs"
+ description: SMB/CIFS Share (Mounts a volume to a SMB share)
+ - variable: read_only
+ label: Read Only
+ description: Mount the volume as read only.
+ schema:
+ type: boolean
+ default: false
+ - variable: mount_path
+ label: Mount Path
+ description: The path inside the container to mount the storage.
+ schema:
+ type: path
+ required: true
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "storage_entry"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: cifs_config
+ label: SMB Configuration
+ description: The configuration for the SMB dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "cifs"]]
+ attrs:
+ - variable: server
+ label: Server
+ description: The server to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: path
+ label: Path
+ description: The path to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: username
+ label: Username
+ description: The username to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: password
+ label: Password
+ description: The password to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ private: true
+ - variable: domain
+ label: Domain
+ description: The domain to use for the SMB share.
+ schema:
+ type: string
+ - variable: labels
+ label: ""
+ group: Labels Configuration
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: label
+ label: Label
+ schema:
+ type: dict
+ attrs:
+ - variable: key
+ label: Key
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: containers
+ label: Containers
+ description: Containers where the label should be applied
+ schema:
+ type: list
+ items:
+ - variable: container
+ label: Container
+ schema:
+ type: string
+ required: true
+ enum:
+ - value: dozzle
+ description: dozzle
+ - variable: resources
+ label: ""
+ group: Resources Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: limits
+ label: Limits
+ schema:
+ type: dict
+ attrs:
+ - variable: cpus
+ label: CPUs
+ description: CPUs limit for Dozzle.
+ schema:
+ type: int
+ default: 2
+ required: true
+ - variable: memory
+ label: Memory (in MB)
+ description: Memory limit for Dozzle.
+ schema:
+ type: int
+ default: 4096
+ required: true
diff --git a/trains/community/dozzle/1.0.0/templates/docker-compose.yaml b/trains/community/dozzle/1.0.0/templates/docker-compose.yaml
new file mode 100644
index 0000000000..dfe50e276a
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/templates/docker-compose.yaml
@@ -0,0 +1,22 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.dozzle_container_name, "image") %}
+
+{% do c1.set_user(values.run_as.user, values.run_as.group) %}
+{% do c1.healthcheck.set_custom_test("/dozzle healthcheck") %}
+
+{% do c1.environment.add_env("DOZZLE_ADDR", ":%d"|format(values.network.web_port.port_number)) %}
+{% do c1.environment.add_env("DOZZLE_MODE", "server") %}
+{% do c1.environment.add_user_envs(values.dozzle.additional_envs) %}
+
+{% do c1.add_docker_socket(read_only=True) %}
+
+{% do c1.add_port(values.network.web_port) %}
+
+{% for store in values.storage.additional_storage %}
+ {% do c1.add_storage(store.mount_path, store) %}
+{% endfor %}
+
+{% do tpl.portals.add_portal({"port": values.network.web_port.port_number}) %}
+
+{{ tpl.render() | tojson }}
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/__init__.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/__init__.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/configs.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/configs.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/container.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/container.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/depends.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/depends.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deploy.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deploy.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_perms.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_redis.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/device.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/device.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/devices.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/devices.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/dns.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/dns.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/environment.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/environment.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/error.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/error.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/expose.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/expose.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/formatter.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/formatter.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/functions.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/functions.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/healthcheck.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/labels.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/labels.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/notes.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/notes.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/portal.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/portal.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/portals.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/portals.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/ports.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/ports.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/render.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/render.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/resources.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/resources.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/restart.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/restart.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/storage.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/storage.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/sysctls.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/validations.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/validations.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_mount.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_sources.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_types.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/volumes.py b/trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/volumes.py
rename to trains/community/dozzle/1.0.0/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/dozzle/1.0.0/templates/test_values/basic-values.yaml b/trains/community/dozzle/1.0.0/templates/test_values/basic-values.yaml
new file mode 100644
index 0000000000..68c6b99ec0
--- /dev/null
+++ b/trains/community/dozzle/1.0.0/templates/test_values/basic-values.yaml
@@ -0,0 +1,20 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+dozzle:
+ additional_envs: []
+
+network:
+ host_network: false
+ web_port:
+ bind_mode: published
+ port_number: 8080
+
+run_as:
+ user: 568
+ group: 568
+
+storage:
+ additional_storage: []
diff --git a/trains/community/dozzle/app_versions.json b/trains/community/dozzle/app_versions.json
new file mode 100644
index 0000000000..5901941604
--- /dev/null
+++ b/trains/community/dozzle/app_versions.json
@@ -0,0 +1,618 @@
+{
+ "1.0.0": {
+ "healthy": true,
+ "supported": true,
+ "healthy_error": null,
+ "location": "/__w/apps/apps/trains/community/dozzle/1.0.0",
+ "last_update": "2025-01-31 17:33:02",
+ "required_features": [],
+ "human_version": "v8.10.5_1.0.0",
+ "version": "1.0.0",
+ "app_metadata": {
+ "app_version": "v8.10.5",
+ "capabilities": [],
+ "categories": [
+ "monitoring"
+ ],
+ "description": "Realtime log viewer for docker containers.",
+ "home": "https://dozzle.dev",
+ "host_mounts": [],
+ "icon": "https://media.sys.truenas.net/apps/dozzle/icons/icon.svg",
+ "keywords": [
+ "logs"
+ ],
+ "lib_version": "2.1.14",
+ "lib_version_hash": "982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc",
+ "maintainers": [
+ {
+ "email": "dev@ixsystems.com",
+ "name": "truenas",
+ "url": "https://www.truenas.com/"
+ }
+ ],
+ "name": "dozzle",
+ "run_as_context": [
+ {
+ "description": "Dozzle runs as any non-root user.",
+ "gid": 568,
+ "group_name": "dozzle",
+ "uid": 568,
+ "user_name": "dozzle"
+ }
+ ],
+ "screenshots": [],
+ "sources": [
+ "https://github.com/amir20/dozzle"
+ ],
+ "title": "Dozzle",
+ "train": "community",
+ "version": "1.0.0"
+ },
+ "schema": {
+ "groups": [
+ {
+ "name": "Dozzle Configuration",
+ "description": "Configure Dozzle"
+ },
+ {
+ "name": "User and Group Configuration",
+ "description": "Configure User and Group for Dozzle"
+ },
+ {
+ "name": "Network Configuration",
+ "description": "Configure Network for Dozzle"
+ },
+ {
+ "name": "Storage Configuration",
+ "description": "Configure Storage for Dozzle"
+ },
+ {
+ "name": "Labels Configuration",
+ "description": "Configure Labels for Dozzle"
+ },
+ {
+ "name": "Resources Configuration",
+ "description": "Configure Resources for Dozzle"
+ }
+ ],
+ "questions": [
+ {
+ "variable": "TZ",
+ "group": "Dozzle Configuration",
+ "label": "Timezone",
+ "schema": {
+ "type": "string",
+ "default": "Etc/UTC",
+ "required": true,
+ "$ref": [
+ "definitions/timezone"
+ ]
+ }
+ },
+ {
+ "variable": "dozzle",
+ "label": "",
+ "group": "Dozzle Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "additional_envs",
+ "label": "Additional Environment Variables",
+ "description": "Configure additional environment variables for Dozzle.",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "env",
+ "label": "Environment Variable",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "name",
+ "label": "Name",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "value",
+ "label": "Value",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "run_as",
+ "label": "",
+ "group": "User and Group Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "user",
+ "label": "User ID",
+ "description": "The user id that Dozzle files will be owned by.",
+ "schema": {
+ "type": "int",
+ "min": 568,
+ "default": 568,
+ "required": true
+ }
+ },
+ {
+ "variable": "group",
+ "label": "Group ID",
+ "description": "The group id that Dozzle files will be owned by.",
+ "schema": {
+ "type": "int",
+ "min": 568,
+ "default": 568,
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "network",
+ "label": "",
+ "group": "Network Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "web_port",
+ "label": "WebUI Port",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "!=",
+ ""
+ ]
+ ],
+ "default": 31100,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ },
+ {
+ "variable": "host_ips",
+ "label": "Host IPs",
+ "description": "IPs on the host to bind this port",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "host_ip",
+ "label": "Host IP",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "$ref": [
+ "definitions/node_bind_ip"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "host_network",
+ "label": "Host Network",
+ "description": "Bind to the host network. It's recommended to keep this disabled.\n",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "storage",
+ "label": "",
+ "group": "Storage Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "additional_storage",
+ "label": "Additional Storage",
+ "description": "Additional storage for Dozzle.",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "storageEntry",
+ "label": "Storage Entry",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "type",
+ "label": "Type",
+ "description": "ixVolume: Is dataset created automatically by the system.\nHost Path: Is a path that already exists on the system.\nSMB Share: Is a SMB share that is mounted to as a volume.\n",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "default": "ix_volume",
+ "immutable": true,
+ "enum": [
+ {
+ "value": "host_path",
+ "description": "Host Path (Path that already exists on the system)"
+ },
+ {
+ "value": "ix_volume",
+ "description": "ixVolume (Dataset created automatically by the system)"
+ },
+ {
+ "value": "cifs",
+ "description": "SMB/CIFS Share (Mounts a volume to a SMB share)"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "read_only",
+ "label": "Read Only",
+ "description": "Mount the volume as read only.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "mount_path",
+ "label": "Mount Path",
+ "description": "The path inside the container to mount the storage.",
+ "schema": {
+ "type": "path",
+ "required": true
+ }
+ },
+ {
+ "variable": "host_path_config",
+ "label": "Host Path Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "host_path"
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "acl",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [],
+ "$ref": [
+ "normalize/acl"
+ ]
+ }
+ },
+ {
+ "variable": "path",
+ "label": "Host Path",
+ "description": "The host path to use for storage.",
+ "schema": {
+ "type": "hostpath",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ false
+ ]
+ ],
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "ix_volume_config",
+ "label": "ixVolume Configuration",
+ "description": "The configuration for the ixVolume dataset.",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "ix_volume"
+ ]
+ ],
+ "$ref": [
+ "normalize/ix_volume"
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "dataset_name",
+ "label": "Dataset Name",
+ "description": "The name of the dataset to use for storage.",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "immutable": true,
+ "default": "storage_entry"
+ }
+ },
+ {
+ "variable": "acl_entries",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [],
+ "$ref": [
+ "normalize/acl"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "cifs_config",
+ "label": "SMB Configuration",
+ "description": "The configuration for the SMB dataset.",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "cifs"
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "server",
+ "label": "Server",
+ "description": "The server to mount the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "path",
+ "label": "Path",
+ "description": "The path to mount the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "username",
+ "label": "Username",
+ "description": "The username to use for the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "password",
+ "label": "Password",
+ "description": "The password to use for the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "private": true
+ }
+ },
+ {
+ "variable": "domain",
+ "label": "Domain",
+ "description": "The domain to use for the SMB share.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "labels",
+ "label": "",
+ "group": "Labels Configuration",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "label",
+ "label": "Label",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "key",
+ "label": "Key",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "value",
+ "label": "Value",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "containers",
+ "label": "Containers",
+ "description": "Containers where the label should be applied",
+ "schema": {
+ "type": "list",
+ "items": [
+ {
+ "variable": "container",
+ "label": "Container",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "enum": [
+ {
+ "value": "dozzle",
+ "description": "dozzle"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "resources",
+ "label": "",
+ "group": "Resources Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "limits",
+ "label": "Limits",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "cpus",
+ "label": "CPUs",
+ "description": "CPUs limit for Dozzle.",
+ "schema": {
+ "type": "int",
+ "default": 2,
+ "required": true
+ }
+ },
+ {
+ "variable": "memory",
+ "label": "Memory (in MB)",
+ "description": "Memory limit for Dozzle.",
+ "schema": {
+ "type": "int",
+ "default": 4096,
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "readme": "Dozzle
Dozzle - Realtime log viewer for docker containers.
",
+ "changelog": null
+ }
+}
\ No newline at end of file
diff --git a/trains/community/dozzle/item.yaml b/trains/community/dozzle/item.yaml
new file mode 100644
index 0000000000..bcd1e0811f
--- /dev/null
+++ b/trains/community/dozzle/item.yaml
@@ -0,0 +1,6 @@
+categories:
+- monitoring
+icon_url: https://media.sys.truenas.net/apps/dozzle/icons/icon.svg
+screenshots: []
+tags:
+- logs
diff --git a/trains/community/drawio/app_versions.json b/trains/community/drawio/app_versions.json
index 5a4cfd01b0..d947af9aef 100644
--- a/trains/community/drawio/app_versions.json
+++ b/trains/community/drawio/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/drawio/1.2.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "26.0.9_1.2.14",
"version": "1.2.14",
diff --git a/trains/community/eclipse-mosquitto/app_versions.json b/trains/community/eclipse-mosquitto/app_versions.json
index 9489a6a564..3533cfbbf9 100644
--- a/trains/community/eclipse-mosquitto/app_versions.json
+++ b/trains/community/eclipse-mosquitto/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/eclipse-mosquitto/1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.0.20_1.0.9",
"version": "1.0.9",
diff --git a/trains/community/esphome/app_versions.json b/trains/community/esphome/app_versions.json
index dfa64a3c1b..55a85f60f2 100644
--- a/trains/community/esphome/app_versions.json
+++ b/trains/community/esphome/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/esphome/1.0.4",
- "last_update": "2025-01-30 08:05:27",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2024.12.4_1.0.4",
"version": "1.0.4",
diff --git a/trains/community/filebrowser/1.2.7/README.md b/trains/community/filebrowser/1.2.8/README.md
similarity index 100%
rename from trains/community/filebrowser/1.2.7/README.md
rename to trains/community/filebrowser/1.2.8/README.md
diff --git a/trains/community/filebrowser/1.2.7/app.yaml b/trains/community/filebrowser/1.2.8/app.yaml
similarity index 97%
rename from trains/community/filebrowser/1.2.7/app.yaml
rename to trains/community/filebrowser/1.2.8/app.yaml
index fd3800f4f5..60e00d9c26 100644
--- a/trains/community/filebrowser/1.2.7/app.yaml
+++ b/trains/community/filebrowser/1.2.8/app.yaml
@@ -1,4 +1,4 @@
-app_version: v2.31.2
+app_version: v2.32.0
capabilities: []
categories:
- storage
@@ -33,4 +33,4 @@ sources:
- https://hub.docker.com/r/filebrowser/filebrowser
title: File Browser
train: community
-version: 1.2.7
+version: 1.2.8
diff --git a/trains/community/filebrowser/1.2.7/ix_values.yaml b/trains/community/filebrowser/1.2.8/ix_values.yaml
similarity index 94%
rename from trains/community/filebrowser/1.2.7/ix_values.yaml
rename to trains/community/filebrowser/1.2.8/ix_values.yaml
index b4127f893c..da02581359 100644
--- a/trains/community/filebrowser/1.2.7/ix_values.yaml
+++ b/trains/community/filebrowser/1.2.8/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: filebrowser/filebrowser
- tag: v2.31.2
+ tag: v2.32.0
consts:
filebrowser_container_name: filebrowser
diff --git a/trains/community/filebrowser/1.2.7/migrations/migrate_from_kubernetes b/trains/community/filebrowser/1.2.8/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migrate_from_kubernetes
rename to trains/community/filebrowser/1.2.8/migrations/migrate_from_kubernetes
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/__init__.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/__init__.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/__init__.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/cpu.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/cpu.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/cpu.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/dns_config.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/dns_config.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/memory.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/memory.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/memory.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/resources.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/resources.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/resources.py
diff --git a/trains/community/filebrowser/1.2.7/migrations/migration_helpers/storage.py b/trains/community/filebrowser/1.2.8/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/filebrowser/1.2.7/migrations/migration_helpers/storage.py
rename to trains/community/filebrowser/1.2.8/migrations/migration_helpers/storage.py
diff --git a/trains/community/filebrowser/1.2.7/questions.yaml b/trains/community/filebrowser/1.2.8/questions.yaml
similarity index 100%
rename from trains/community/filebrowser/1.2.7/questions.yaml
rename to trains/community/filebrowser/1.2.8/questions.yaml
diff --git a/trains/community/filebrowser/1.2.7/templates/docker-compose.yaml b/trains/community/filebrowser/1.2.8/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/docker-compose.yaml
rename to trains/community/filebrowser/1.2.8/templates/docker-compose.yaml
diff --git a/trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/glances/1.0.1/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/configs.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/configs.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/container.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/container.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/depends.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/depends.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deploy.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deploy.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_mariadb.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_mariadb.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_perms.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_perms.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_postgres.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_postgres.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_redis.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/deps_redis.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/device.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/device.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/devices.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/devices.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/dns.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/dns.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/environment.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/environment.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/error.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/error.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/expose.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/expose.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/formatter.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/formatter.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/functions.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/functions.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/healthcheck.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/healthcheck.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/labels.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/labels.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/notes.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/notes.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/portal.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/portal.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/portals.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/portals.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/ports.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/ports.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/render.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/render.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/resources.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/resources.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/restart.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/restart.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/storage.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/storage.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/sysctls.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/sysctls.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/__init__.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/__init__.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_build_image.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_build_image.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_configs.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_configs.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_depends.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_depends.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_deps.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_deps.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_device.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_device.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_dns.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_dns.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_environment.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_environment.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_expose.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_expose.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_formatter.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_formatter.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_functions.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_functions.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_healthcheck.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_healthcheck.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_labels.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_labels.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_notes.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_notes.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_portal.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_portal.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_ports.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_ports.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_render.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_render.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_resources.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_resources.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_restart.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_restart.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_sysctls.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_sysctls.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_validations.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_validations.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_volumes.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_volumes.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/validations.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/validations.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_mount.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_mount.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_mount_types.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_mount_types.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_sources.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_types.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_types.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volumes.py b/trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volumes.py
rename to trains/community/filebrowser/1.2.8/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/filebrowser/1.2.7/templates/macros/script.sh b/trains/community/filebrowser/1.2.8/templates/macros/script.sh
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/macros/script.sh
rename to trains/community/filebrowser/1.2.8/templates/macros/script.sh
diff --git a/trains/community/filebrowser/1.2.7/templates/test_values/basic-values.yaml b/trains/community/filebrowser/1.2.8/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/test_values/basic-values.yaml
rename to trains/community/filebrowser/1.2.8/templates/test_values/basic-values.yaml
diff --git a/trains/community/filebrowser/1.2.7/templates/test_values/https-values.yaml b/trains/community/filebrowser/1.2.8/templates/test_values/https-values.yaml
similarity index 100%
rename from trains/community/filebrowser/1.2.7/templates/test_values/https-values.yaml
rename to trains/community/filebrowser/1.2.8/templates/test_values/https-values.yaml
diff --git a/trains/community/filebrowser/app_versions.json b/trains/community/filebrowser/app_versions.json
index cd46574aa5..3feb039528 100644
--- a/trains/community/filebrowser/app_versions.json
+++ b/trains/community/filebrowser/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.2.7": {
+ "1.2.8": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/filebrowser/1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/filebrowser/1.2.8",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "v2.31.2_1.2.7",
- "version": "1.2.7",
+ "human_version": "v2.32.0_1.2.8",
+ "version": "1.2.8",
"app_metadata": {
- "app_version": "v2.31.2",
+ "app_version": "v2.32.0",
"capabilities": [],
"categories": [
"storage"
@@ -53,7 +53,7 @@
],
"title": "File Browser",
"train": "community",
- "version": "1.2.7"
+ "version": "1.2.8"
},
"schema": {
"groups": [
diff --git a/trains/community/filestash/app_versions.json b/trains/community/filestash/app_versions.json
index 827a4683bc..35b346765e 100644
--- a/trains/community/filestash/app_versions.json
+++ b/trains/community/filestash/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/filestash/1.0.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.0.11",
"version": "1.0.11",
diff --git a/trains/community/firefly-iii/app_versions.json b/trains/community/firefly-iii/app_versions.json
index aa4e1e4997..8b4a8500ae 100644
--- a/trains/community/firefly-iii/app_versions.json
+++ b/trains/community/firefly-iii/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/firefly-iii/1.4.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "version-6.1.25_1.4.11",
"version": "1.4.11",
diff --git a/trains/community/flame/app_versions.json b/trains/community/flame/app_versions.json
index c96eed3ea3..de28632f93 100644
--- a/trains/community/flame/app_versions.json
+++ b/trains/community/flame/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/flame/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.3.1_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/flaresolverr/app_versions.json b/trains/community/flaresolverr/app_versions.json
index 25bd924028..a65fb8594d 100644
--- a/trains/community/flaresolverr/app_versions.json
+++ b/trains/community/flaresolverr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/flaresolverr/1.0.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v3.3.21_1.0.16",
"version": "1.0.16",
diff --git a/trains/community/freshrss/app_versions.json b/trains/community/freshrss/app_versions.json
index 57102ae5af..9969d72061 100644
--- a/trains/community/freshrss/app_versions.json
+++ b/trains/community/freshrss/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/freshrss/1.3.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.25.0_1.3.7",
"version": "1.3.7",
diff --git a/trains/community/frigate/1.1.14/README.md b/trains/community/frigate/1.1.15/README.md
similarity index 100%
rename from trains/community/frigate/1.1.14/README.md
rename to trains/community/frigate/1.1.15/README.md
diff --git a/trains/community/frigate/1.1.14/app.yaml b/trains/community/frigate/1.1.15/app.yaml
similarity index 98%
rename from trains/community/frigate/1.1.14/app.yaml
rename to trains/community/frigate/1.1.15/app.yaml
index 1cde643063..eaedb8431a 100644
--- a/trains/community/frigate/1.1.14/app.yaml
+++ b/trains/community/frigate/1.1.15/app.yaml
@@ -42,4 +42,4 @@ sources:
- https://github.com/blakeblackshear/frigate
title: Frigate
train: community
-version: 1.1.14
+version: 1.1.15
diff --git a/trains/community/frigate/1.1.14/ix_values.yaml b/trains/community/frigate/1.1.15/ix_values.yaml
similarity index 79%
rename from trains/community/frigate/1.1.14/ix_values.yaml
rename to trains/community/frigate/1.1.15/ix_values.yaml
index 64dc0b9035..0fb3e2c690 100644
--- a/trains/community/frigate/1.1.14/ix_values.yaml
+++ b/trains/community/frigate/1.1.15/ix_values.yaml
@@ -16,3 +16,5 @@ consts:
notes_body: |
Default credentials are printed in the logs
during the first run of the application.
+ ssl_key_path: /etc/letsencrypt/live/frigate/privkey.pem
+ ssl_cert_path: /etc/letsencrypt/live/frigate/fullchain.pem
diff --git a/trains/community/frigate/1.1.14/migrations/migrate_from_kubernetes b/trains/community/frigate/1.1.15/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migrate_from_kubernetes
rename to trains/community/frigate/1.1.15/migrations/migrate_from_kubernetes
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/__init__.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/__init__.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/__init__.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/cpu.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/cpu.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/cpu.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/dns_config.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/dns_config.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/memory.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/memory.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/memory.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/resources.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/resources.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/resources.py
diff --git a/trains/community/frigate/1.1.14/migrations/migration_helpers/storage.py b/trains/community/frigate/1.1.15/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/frigate/1.1.14/migrations/migration_helpers/storage.py
rename to trains/community/frigate/1.1.15/migrations/migration_helpers/storage.py
diff --git a/trains/community/frigate/1.1.14/questions.yaml b/trains/community/frigate/1.1.15/questions.yaml
similarity index 99%
rename from trains/community/frigate/1.1.14/questions.yaml
rename to trains/community/frigate/1.1.15/questions.yaml
index 0e78728192..1d6577ef87 100644
--- a/trains/community/frigate/1.1.14/questions.yaml
+++ b/trains/community/frigate/1.1.15/questions.yaml
@@ -194,6 +194,14 @@ questions:
required: true
$ref:
- definitions/port
+ - variable: certificate_id
+ label: Certificate
+ description: The certificate to use for Frigate.
+ schema:
+ type: int
+ "null": true
+ $ref:
+ - "definitions/certificate"
- variable: storage
label: ""
diff --git a/trains/community/frigate/1.1.14/templates/docker-compose.yaml b/trains/community/frigate/1.1.15/templates/docker-compose.yaml
similarity index 89%
rename from trains/community/frigate/1.1.14/templates/docker-compose.yaml
rename to trains/community/frigate/1.1.15/templates/docker-compose.yaml
index d84453f984..899861232e 100644
--- a/trains/community/frigate/1.1.14/templates/docker-compose.yaml
+++ b/trains/community/frigate/1.1.15/templates/docker-compose.yaml
@@ -41,6 +41,12 @@
{% endif %}
{% endif %}
+{% if values.network.certificate_id %}
+ {% set cert = values.ix_certificates[values.network.certificate_id] %}
+ {% do c1.configs.add("private", cert.privatekey, values.consts.ssl_key_path) %}
+ {% do c1.configs.add("public", cert.certificate, values.consts.ssl_cert_path) %}
+{% endif %}
+
{% do tpl.portals.add_portal({"port": values.consts.internal_web_port if values.network.host_network else values.network.web_port, "scheme": "https"}) %}
{% if values.network.enable_no_auth %}
{% do tpl.portals.add_portal({"name": "Web UI (No Auth)", "port": values.consts.internal_no_auth_port if values.network.host_network else values.network.no_auth_port}) %}
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/__init__.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/__init__.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/configs.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/configs.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/container.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/container.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/depends.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/depends.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deploy.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deploy.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_perms.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_redis.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/device.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/device.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/devices.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/devices.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/dns.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/dns.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/environment.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/environment.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/error.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/error.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/expose.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/expose.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/formatter.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/formatter.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/functions.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/functions.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/healthcheck.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/labels.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/labels.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/notes.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/notes.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/portal.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/portal.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/portals.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/portals.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/ports.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/ports.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/render.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/render.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/resources.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/resources.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/restart.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/restart.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/storage.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/storage.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/sysctls.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/__init__.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/__init__.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/validations.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/validations.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_mount.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_sources.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_types.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/volumes.py b/trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/volumes.py
rename to trains/community/frigate/1.1.15/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/frigate/1.1.14/templates/test_values/basic-values.yaml b/trains/community/frigate/1.1.15/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/frigate/1.1.14/templates/test_values/basic-values.yaml
rename to trains/community/frigate/1.1.15/templates/test_values/basic-values.yaml
diff --git a/trains/community/frigate/1.1.15/templates/test_values/https-values.yaml b/trains/community/frigate/1.1.15/templates/test_values/https-values.yaml
new file mode 100644
index 0000000000..1e9e52fe23
--- /dev/null
+++ b/trains/community/frigate/1.1.15/templates/test_values/https-values.yaml
@@ -0,0 +1,130 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+frigate:
+ image_selector: image
+ shm_size_mb: 64
+ mount_usb_bus: false
+ additional_envs: []
+
+network:
+ host_network: false
+ web_port: 8081
+ enable_no_auth: true
+ no_auth_port: 8080
+ enable_rtsp: true
+ rtsp_port: 8554
+ enable_webrtc: true
+ webrtc_port: 8082
+ enable_go2rtc: true
+ go2rtc_port: 8083
+ certificate_id: "2"
+
+ix_volumes:
+ frigate-config: /opt/tests/mnt/config
+ frigate-media: /opt/tests/mnt/media
+
+storage:
+ config:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: frigate-config
+ create_host_path: true
+ media:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: frigate-media
+ create_host_path: true
+ cache:
+ type: tmpfs
+ tmpfs_config:
+ size: 1024
+ additional_storage: []
+
+ix_certificates:
+ "2":
+ certificate: |
+ -----BEGIN CERTIFICATE-----
+ MIIEdjCCA16gAwIBAgIDYFMYMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+ ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+ VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ HhcNMjEwODMwMjMyMzU0WhcNMjMxMjAzMjMyMzU0WjBuMQswCQYDVQQDDAJhZDEL
+ MAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxDTALBgNVBAcMBGFzZGYxDTALBgNV
+ BAoMBGFkc2YxDTALBgNVBAsMBGFzZGYxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7+1xOHRQyOnQTHFcrdasX
+ Zl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/AbkH7oVFWC1
+ P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI4vQCdYgW
+ 2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2dNgsxKU0H
+ PGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB+Zie331t
+ AzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7A/FuDVg3
+ AgMBAAGjggEdMIIBGTAnBgNVHREEIDAehwTAqAADhwTAqAAFhwTAqAC2hwTAqACB
+ hwTAqACSMB0GA1UdDgQWBBQ4G2ff4tgZl4vmo4xCfqmJhdqShzAMBgNVHRMBAf8E
+ AjAAMIGYBgNVHSMEgZAwgY2AFLlYf9L99nxJDcpCM/LT3V5hQ/a3oXCkbjBsMQww
+ CgYDVQQDDANhc2QxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQH
+ DAJhZjENMAsGA1UECgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkB
+ FgdhQGEuY29tggNgUxcwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/
+ BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBAQA6FpOInEHB5iVk3FP67GybJ29vHZTD
+ KQHbQgmg8s4L7qIsA1HQ+DMCbdylpA11x+t/eL/n48BvGw2FNXpN6uykhLHJjbKR
+ h8yITa2KeD3LjLYhScwIigXmTVYSP3km6s8jRL6UKT9zttnIHyXVpBDya6Q4WTMx
+ fmfC6O7t1PjQ5ZyVtzizIUP8ah9n4TKdXU4A3QIM6WsJXpHb+vqp1WDWJ7mKFtgj
+ x5TKv3wcPnktx0zMPfLb5BTSE9rc9djcBG0eIAsPT4FgiatCUChe7VhuMnqskxEz
+ MymJLoq8+mzucRwFkOkR2EIt1x+Irl2mJVMeBow63rVZfUQBD8h++LqB
+ -----END CERTIFICATE-----
+ -----BEGIN CERTIFICATE-----
+ MIIEhDCCA2ygAwIBAgIDYFMXMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz
+ ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD
+ VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w
+ HhcNMjEwODMwMjMyMDQ1WhcNMzEwODI4MjMyMDQ1WjBsMQwwCgYDVQQDDANhc2Qx
+ CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQHDAJhZjENMAsGA1UE
+ CgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkBFgdhQGEuY29tMIIB
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq//c0hEEr83CS1pMgsHX50jt
+ 2MqIbcf63UUNJTiYpUUvUQSFJFc7m/dr+RTZvu97eDCnD5K2qkHHvTPaPZwY+Djf
+ iy7N641Sz6u/y3Yo3xxs1Aermsfedh48vusJpjbkT2XS44VjbkrpKcWDNVpp3Evd
+ M7oJotXeUsZ+imiyVCfr4YhoY5gbGh/r+KN9Wf9YKoUyfLLZGwdZkhtX2zIbidsL
+ Thqi9YTaUHttGinjiBBum234u/CfvKXsfG3yP2gvBGnlvZnM9ktv+lVffYNqlf7H
+ VmB1bKKk84HtzuW5X76SGAgOG8eHX4x5ZLI1WQUuoQOVRl1I0UCjBtbz8XhwvQID
+ AQABo4IBLTCCASkwLQYDVR0RBCYwJIcEwKgABYcEwKgAA4cEwKgAkocEwKgAtYcE
+ wKgAgYcEwKgAtjAdBgNVHQ4EFgQUuVh/0v32fEkNykIz8tPdXmFD9rcwDwYDVR0T
+ AQH/BAUwAwEB/zCBmAYDVR0jBIGQMIGNgBS5WH/S/fZ8SQ3KQjPy091eYUP2t6Fw
+ pG4wbDEMMAoGA1UEAwwDYXNkMQswCQYDVQQGEwJVUzENMAsGA1UECAwEYXNkZjEL
+ MAkGA1UEBwwCYWYxDTALBgNVBAoMBGFzZGYxDDAKBgNVBAsMA2FzZDEWMBQGCSqG
+ SIb3DQEJARYHYUBhLmNvbYIDYFMXMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+ BQcDAjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAKEocOmVuWlr
+ zegtKYMe8NhHIkFY9oVn5ym6RHNOJpPH4QF8XYC3Z5+iC5yGh4P/jVe/4I4SF6Ql
+ PtofU0jNq5vzapt/y+m008eXqPQFmoUOvu+JavoRVcRx2LIP5AgBA1mF56CSREsX
+ TkuJAA9IUQ8EjnmAoAeKINuPaKxGDuU8BGCMqr/qd564MKNf9XYL+Fb2rlkA0O2d
+ 2No34DQLgqSmST/LAvPM7Cbp6knYgnKmGr1nETCXasg1cueHLnWWTvps2HiPp2D/
+ +Fq0uqcZLu4Mdo0CPs4e5sHRyldEnRSKh0DVLprq9zr/GMipmPLJUsT5Jed3sj0w
+ M7Y3vwxshpo=
+ -----END CERTIFICATE-----
+ privatekey: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7+1xOHRQyOnQT
+ HFcrdasXZl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/Abk
+ H7oVFWC1P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI
+ 4vQCdYgW2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2d
+ NgsxKU0HPGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB
+ +Zie331tAzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7
+ A/FuDVg3AgMBAAECggEAapt30rj9DitGTtxAt13pJMEhyYxvvD3WkvmJwguF/Bbu
+ eW0Ba1c668fMeRCA54FWi1sMqusPS4HUqqUvk+tmyAOsAF4qgD/A4MMSC7uJSVI5
+ N/JWhJWyhCY94/FPakiO1nbPbVw41bcqtzU2qvparpME2CtxSCbDiqm7aaag3Kqe
+ EF0fGSUdZ+TYl9JM05+eIyiX+UY19Fg0OjTHMn8nGpxcNTfDBdQ68TKvdo/dtIKL
+ PLKzJUNNdM8odC4CvQtfGMqaslwZwXkiOl5VJcW21ncj/Y0ngEMKeD/i65ZoqGdR
+ 0FKCQYEAGtM2FvJcZQ92Wsw7yj2bK2MSegVUyLK32QKBgQDe8syVCepPzRsfjfxA
+ 6TZlWcGuTZLhwIx97Ktw3VcQ1f4rLoEYlv0xC2VWBORpzIsJo4I/OLmgp8a+Ga8z
+ FkVRnq90dV3t4NP9uJlHgcODHnOardC2UUka4olBSCG6zmK4Jxi34lOxhGRkshOo
+ L4IBeOIB5g+ZrEEXkzfYJHESRQKBgQDX2YhFhGIrT8BAnC5BbXbhm8h6Bhjz8DYL
+ d+qhVJjef7L/aJxViU0hX9Ba2O8CLK3FZeREFE3hJPiJ4TZSlN4evxs5p+bbNDcA
+ 0mhRI/o3X4ac6IxdRebyYnCOB/Cu94/MzppcZcotlCekKNike7eorCcX4Qavm7Pu
+ MUuQ+ifmSwKBgEnchoqZzlbBzMqXb4rRuIO7SL9GU/MWp3TQg7vQmJerTZlgvsQ2
+ wYsOC3SECmhCq4117iCj2luvOdihCboTFsQDnn0mpQe6BIF6Ns3J38wAuqv0CcFd
+ DKsrge1uyD3rQilgSoAhKzkUc24o0PpXQurZ8YZPgbuXpbj5vPaOnCdBAoGACYc7
+ wb3XS4wos3FxhUfcwJbM4b4VKeeHqzfu7pI6cU/3ydiHVitKcVe2bdw3qMPqI9Wc
+ nvi6e17Tbdq4OCsEJx1OiVwFD9YdO3cOTc6lw/3+hjypvZBRYo+/4jUthbu96E+S
+ dtOzehGZMmDvN0uSzupSi3ZOgkAAUFpyuIKickMCgYAId0PCRjonO2thn/R0rZ7P
+ //L852uyzYhXKw5/fjFGhQ6LbaLgIRFaCZ0L2809u0HFnNvJjHv4AKP6j+vFQYYY
+ qQ+66XnfsA9G/bu4MDS9AX83iahD9IdLXQAy8I19prAbpVumKegPbMnNYNB/TYEc
+ 3G15AKCXo7jjOUtHY01DCQ==
+ -----END PRIVATE KEY-----
diff --git a/trains/community/frigate/app_versions.json b/trains/community/frigate/app_versions.json
index 1446ff3eab..89eb8256b5 100644
--- a/trains/community/frigate/app_versions.json
+++ b/trains/community/frigate/app_versions.json
@@ -1,13 +1,13 @@
{
- "1.1.14": {
+ "1.1.15": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/frigate/1.1.14",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/frigate/1.1.15",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "0.14.1_1.1.14",
- "version": "1.1.14",
+ "human_version": "0.14.1_1.1.15",
+ "version": "1.1.15",
"app_metadata": {
"app_version": "0.14.1",
"capabilities": [
@@ -76,7 +76,7 @@
],
"title": "Frigate",
"train": "community",
- "version": "1.1.14"
+ "version": "1.1.15"
},
"schema": {
"groups": [
@@ -385,6 +385,18 @@
}
]
}
+ },
+ {
+ "variable": "certificate_id",
+ "label": "Certificate",
+ "description": "The certificate to use for Frigate.",
+ "schema": {
+ "type": "int",
+ "null": true,
+ "$ref": [
+ "definitions/certificate"
+ ]
+ }
}
]
}
diff --git a/trains/community/fscrawler/app_versions.json b/trains/community/fscrawler/app_versions.json
index a535da911e..4710066954 100644
--- a/trains/community/fscrawler/app_versions.json
+++ b/trains/community/fscrawler/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/fscrawler/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.10-SNAPSHOT-ocr-es7_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/gaseous-server/app_versions.json b/trains/community/gaseous-server/app_versions.json
index efa3204e24..340d39c3a9 100644
--- a/trains/community/gaseous-server/app_versions.json
+++ b/trains/community/gaseous-server/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/gaseous-server/1.0.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.7.9_1.0.16",
"version": "1.0.16",
diff --git a/trains/community/gitea-act-runner/app_versions.json b/trains/community/gitea-act-runner/app_versions.json
index aded4886b4..5ed3dee104 100644
--- a/trains/community/gitea-act-runner/app_versions.json
+++ b/trains/community/gitea-act-runner/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/gitea-act-runner/1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.2.11_1.0.2",
"version": "1.0.2",
diff --git a/trains/community/gitea/app_versions.json b/trains/community/gitea/app_versions.json
index 65933af8ad..30a836e70e 100644
--- a/trains/community/gitea/app_versions.json
+++ b/trains/community/gitea/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/gitea/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.23.1_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/glances/1.0.1/README.md b/trains/community/glances/1.0.2/README.md
similarity index 100%
rename from trains/community/glances/1.0.1/README.md
rename to trains/community/glances/1.0.2/README.md
diff --git a/trains/community/glances/1.0.1/app.yaml b/trains/community/glances/1.0.2/app.yaml
similarity index 97%
rename from trains/community/glances/1.0.1/app.yaml
rename to trains/community/glances/1.0.2/app.yaml
index 57c88f408f..1f0b68f454 100644
--- a/trains/community/glances/1.0.1/app.yaml
+++ b/trains/community/glances/1.0.2/app.yaml
@@ -1,4 +1,4 @@
-app_version: 4.3.0.7
+app_version: 4.3.0.8
capabilities:
- description: Glances is able to bypass permission checks for it's sub-processes.
name: FOWNER
@@ -46,4 +46,4 @@ sources:
- https://hub.docker.com/r/nicolargo/glances
title: Glances
train: community
-version: 1.0.1
+version: 1.0.2
diff --git a/trains/community/glances/1.0.1/ix_values.yaml b/trains/community/glances/1.0.2/ix_values.yaml
similarity index 83%
rename from trains/community/glances/1.0.1/ix_values.yaml
rename to trains/community/glances/1.0.2/ix_values.yaml
index 9d3112e3af..a4018a3a62 100644
--- a/trains/community/glances/1.0.1/ix_values.yaml
+++ b/trains/community/glances/1.0.2/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: nicolargo/glances
- tag: "4.3.0.7"
+ tag: "4.3.0.8"
consts:
glances_container_name: glances
diff --git a/trains/community/glances/1.0.1/questions.yaml b/trains/community/glances/1.0.2/questions.yaml
similarity index 100%
rename from trains/community/glances/1.0.1/questions.yaml
rename to trains/community/glances/1.0.2/questions.yaml
diff --git a/trains/community/glances/1.0.1/templates/docker-compose.yaml b/trains/community/glances/1.0.2/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/glances/1.0.1/templates/docker-compose.yaml
rename to trains/community/glances/1.0.2/templates/docker-compose.yaml
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/__init__.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/__init__.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/configs.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/configs.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/container.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/container.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/depends.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/depends.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deploy.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deploy.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_perms.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_redis.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/device.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/device.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/devices.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/devices.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/dns.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/dns.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/environment.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/environment.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/error.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/error.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/expose.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/expose.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/formatter.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/formatter.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/functions.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/functions.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/healthcheck.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/labels.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/labels.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/notes.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/notes.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/portal.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/portal.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/portals.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/portals.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/ports.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/ports.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/render.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/render.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/resources.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/resources.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/restart.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/restart.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/storage.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/storage.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/sysctls.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/validations.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/validations.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_mount.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_sources.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_types.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volumes.py b/trains/community/glances/1.0.2/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/volumes.py
rename to trains/community/glances/1.0.2/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/glances/1.0.1/templates/test_values/basic-values.yaml b/trains/community/glances/1.0.2/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/glances/1.0.1/templates/test_values/basic-values.yaml
rename to trains/community/glances/1.0.2/templates/test_values/basic-values.yaml
diff --git a/trains/community/glances/app_versions.json b/trains/community/glances/app_versions.json
index c019a2e180..ac3cbc569a 100644
--- a/trains/community/glances/app_versions.json
+++ b/trains/community/glances/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.0.1": {
+ "1.0.2": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/glances/1.0.1",
- "last_update": "2025-01-30 08:05:27",
+ "location": "/__w/apps/apps/trains/community/glances/1.0.2",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "4.3.0.7_1.0.1",
- "version": "1.0.1",
+ "human_version": "4.3.0.8_1.0.2",
+ "version": "1.0.2",
"app_metadata": {
- "app_version": "4.3.0.7",
+ "app_version": "4.3.0.8",
"capabilities": [
{
"description": "Glances is able to bypass permission checks for it's sub-processes.",
@@ -87,7 +87,7 @@
],
"title": "Glances",
"train": "community",
- "version": "1.0.1"
+ "version": "1.0.2"
},
"schema": {
"groups": [
diff --git a/trains/community/grafana/app_versions.json b/trains/community/grafana/app_versions.json
index 558cc040ce..60e1b2f2d3 100644
--- a/trains/community/grafana/app_versions.json
+++ b/trains/community/grafana/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/grafana/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "11.5.0_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/handbrake-web/app_versions.json b/trains/community/handbrake-web/app_versions.json
index 8b929a226c..9189a59d45 100644
--- a/trains/community/handbrake-web/app_versions.json
+++ b/trains/community/handbrake-web/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/handbrake-web/1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.7.3_1.0.2",
"version": "1.0.2",
diff --git a/trains/community/handbrake/app_versions.json b/trains/community/handbrake/app_versions.json
index e90c284534..55462677a0 100644
--- a/trains/community/handbrake/app_versions.json
+++ b/trains/community/handbrake/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/handbrake/2.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v24.12.1_2.1.8",
"version": "2.1.8",
diff --git a/trains/community/homarr/app_versions.json b/trains/community/homarr/app_versions.json
index c06c16644b..28e541165a 100644
--- a/trains/community/homarr/app_versions.json
+++ b/trains/community/homarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/homarr/2.0.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.3.1_2.0.7",
"version": "2.0.7",
diff --git a/trains/community/homepage/app_versions.json b/trains/community/homepage/app_versions.json
index 8dd66707cd..da61990178 100644
--- a/trains/community/homepage/app_versions.json
+++ b/trains/community/homepage/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/homepage/1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v0.10.9_1.1.15",
"version": "1.1.15",
diff --git a/trains/community/homer/app_versions.json b/trains/community/homer/app_versions.json
index 725e4daf61..e7d544804f 100644
--- a/trains/community/homer/app_versions.json
+++ b/trains/community/homer/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/homer/2.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v24.12.1_2.1.9",
"version": "2.1.9",
diff --git a/trains/community/iconik-storage-gateway/1.0.9/README.md b/trains/community/iconik-storage-gateway/1.0.10/README.md
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/README.md
rename to trains/community/iconik-storage-gateway/1.0.10/README.md
diff --git a/trains/community/iconik-storage-gateway/1.0.9/app.yaml b/trains/community/iconik-storage-gateway/1.0.10/app.yaml
similarity index 84%
rename from trains/community/iconik-storage-gateway/1.0.9/app.yaml
rename to trains/community/iconik-storage-gateway/1.0.10/app.yaml
index 7eeb4c2cc3..ac9400a1a4 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/app.yaml
+++ b/trains/community/iconik-storage-gateway/1.0.10/app.yaml
@@ -1,4 +1,4 @@
-app_version: 3.12.0
+app_version: 3.12.1
capabilities: []
categories:
- productivity
@@ -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.9
-lib_version_hash: 6bd4d433db7dce2d4b8cc456a0f7874b45d52a6e2b145d5a1f48f327654a7125
+lib_version: 2.1.14
+lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
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.9
+version: 1.0.10
diff --git a/trains/community/iconik-storage-gateway/1.0.9/ix_values.yaml b/trains/community/iconik-storage-gateway/1.0.10/ix_values.yaml
similarity index 90%
rename from trains/community/iconik-storage-gateway/1.0.9/ix_values.yaml
rename to trains/community/iconik-storage-gateway/1.0.10/ix_values.yaml
index 0e50978698..978d11987c 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/ix_values.yaml
+++ b/trains/community/iconik-storage-gateway/1.0.10/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ghcr.io/truenas/iconik-storage-gateway-docker
- tag: 3.12.0
+ tag: 3.12.1
consts:
iconik_container_name: iconik
diff --git a/trains/community/iconik-storage-gateway/1.0.9/questions.yaml b/trains/community/iconik-storage-gateway/1.0.10/questions.yaml
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/questions.yaml
rename to trains/community/iconik-storage-gateway/1.0.10/questions.yaml
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/docker-compose.yaml b/trains/community/iconik-storage-gateway/1.0.10/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/docker-compose.yaml
rename to trains/community/iconik-storage-gateway/1.0.10/templates/docker-compose.yaml
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/__init__.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/__init__.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/configs.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/configs.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/container.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/container.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/depends.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/depends.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deploy.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deploy.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_perms.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_redis.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/device.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/device.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/devices.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/devices.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/dns.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/dns.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/environment.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/environment.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/error.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/error.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/expose.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/expose.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/formatter.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/formatter.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/functions.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/functions.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/healthcheck.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/labels.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/labels.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/notes.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/notes.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/portal.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/portal.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/portals.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/portals.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/ports.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/ports.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/render.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/render.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/resources.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/resources.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/restart.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/restart.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/storage.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/storage.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/sysctls.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/__init__.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/__init__.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/validations.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/validations.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_mount.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_sources.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_types.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volumes.py b/trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/volumes.py
rename to trains/community/iconik-storage-gateway/1.0.10/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/test_values/basic-values.yaml b/trains/community/iconik-storage-gateway/1.0.10/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/test_values/basic-values.yaml
rename to trains/community/iconik-storage-gateway/1.0.10/templates/test_values/basic-values.yaml
diff --git a/trains/community/iconik-storage-gateway/app_versions.json b/trains/community/iconik-storage-gateway/app_versions.json
index afb54eaa5b..6f243ac3b5 100644
--- a/trains/community/iconik-storage-gateway/app_versions.json
+++ b/trains/community/iconik-storage-gateway/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.0.9": {
+ "1.0.10": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/iconik-storage-gateway/1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/iconik-storage-gateway/1.0.10",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "3.12.0_1.0.9",
- "version": "1.0.9",
+ "human_version": "3.12.1_1.0.10",
+ "version": "1.0.10",
"app_metadata": {
- "app_version": "3.12.0",
+ "app_version": "3.12.1",
"capabilities": [],
"categories": [
"productivity"
@@ -21,8 +21,8 @@
"keywords": [
"iconik"
],
- "lib_version": "2.1.9",
- "lib_version_hash": "6bd4d433db7dce2d4b8cc456a0f7874b45d52a6e2b145d5a1f48f327654a7125",
+ "lib_version": "2.1.14",
+ "lib_version_hash": "982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc",
"maintainers": [
{
"email": "dev@ixsystems.com",
@@ -47,7 +47,7 @@
],
"title": "Iconik Storage Gateway",
"train": "community",
- "version": "1.0.9"
+ "version": "1.0.10"
},
"schema": {
"groups": [
diff --git a/trains/community/immich/1.7.24/README.md b/trains/community/immich/1.7.25/README.md
similarity index 100%
rename from trains/community/immich/1.7.24/README.md
rename to trains/community/immich/1.7.25/README.md
diff --git a/trains/community/immich/1.7.24/app.yaml b/trains/community/immich/1.7.25/app.yaml
similarity index 96%
rename from trains/community/immich/1.7.24/app.yaml
rename to trains/community/immich/1.7.25/app.yaml
index 8771b70c4f..fca2fa7124 100644
--- a/trains/community/immich/1.7.24/app.yaml
+++ b/trains/community/immich/1.7.25/app.yaml
@@ -1,4 +1,4 @@
-app_version: v1.125.6
+app_version: v1.125.7
capabilities:
- description: Immich Proxy is able to chown files.
name: CHOWN
@@ -45,4 +45,4 @@ sources:
- https://github.com/immich-app/immich
title: Immich
train: community
-version: 1.7.24
+version: 1.7.25
diff --git a/trains/community/immich/1.7.24/ix_values.yaml b/trains/community/immich/1.7.25/ix_values.yaml
similarity index 88%
rename from trains/community/immich/1.7.24/ix_values.yaml
rename to trains/community/immich/1.7.25/ix_values.yaml
index 8f09af8cd4..f1cdc2ba6a 100644
--- a/trains/community/immich/1.7.24/ix_values.yaml
+++ b/trains/community/immich/1.7.25/ix_values.yaml
@@ -1,16 +1,16 @@
images:
image:
repository: ghcr.io/immich-app/immich-server
- tag: v1.125.6
+ tag: v1.125.7
ml_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6
+ tag: v1.125.7
ml_cuda_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6-cuda
+ tag: v1.125.7-cuda
ml_openvino_image:
repository: ghcr.io/immich-app/immich-machine-learning
- tag: v1.125.6-openvino
+ tag: v1.125.7-openvino
pgvecto_image:
repository: tensorchord/pgvecto-rs
tag: pg15-v0.2.0
diff --git a/trains/community/immich/1.7.24/migrations/migrate_from_kubernetes b/trains/community/immich/1.7.25/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migrate_from_kubernetes
rename to trains/community/immich/1.7.25/migrations/migrate_from_kubernetes
diff --git a/trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/immich/1.7.25/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/__init__.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/cpu.py b/trains/community/immich/1.7.25/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/cpu.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/cpu.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/dns_config.py b/trains/community/immich/1.7.25/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/dns_config.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/immich/1.7.25/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/memory.py b/trains/community/immich/1.7.25/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/memory.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/memory.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/resources.py b/trains/community/immich/1.7.25/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/resources.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/resources.py
diff --git a/trains/community/immich/1.7.24/migrations/migration_helpers/storage.py b/trains/community/immich/1.7.25/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/immich/1.7.24/migrations/migration_helpers/storage.py
rename to trains/community/immich/1.7.25/migrations/migration_helpers/storage.py
diff --git a/trains/community/immich/1.7.24/questions.yaml b/trains/community/immich/1.7.25/questions.yaml
similarity index 100%
rename from trains/community/immich/1.7.24/questions.yaml
rename to trains/community/immich/1.7.25/questions.yaml
diff --git a/trains/community/immich/1.7.24/templates/docker-compose.yaml b/trains/community/immich/1.7.25/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/immich/1.7.24/templates/docker-compose.yaml
rename to trains/community/immich/1.7.25/templates/docker-compose.yaml
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/__init__.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/__init__.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/__init__.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/__init__.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/environment.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/environment.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/environment.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/environment.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/healthchecks.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/healthchecks.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/healthchecks.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/healthchecks.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/mariadb.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/mariadb.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/mariadb.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/mariadb.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/metadata.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/metadata.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/metadata.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/metadata.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/network.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/network.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/network.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/network.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/permissions.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/permissions.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/permissions.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/permissions.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/ports.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/ports.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/ports.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/ports.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/postgres.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/postgres.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/postgres.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/postgres.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/redis.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/redis.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/redis.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/redis.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/resources.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/resources.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/resources.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/resources.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/security.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/security.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/security.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/security.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/storage.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/storage.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/storage.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/storage.py
diff --git a/trains/community/immich/1.7.24/templates/library/base_v1_1_7/utils.py b/trains/community/immich/1.7.25/templates/library/base_v1_1_7/utils.py
similarity index 100%
rename from trains/community/immich/1.7.24/templates/library/base_v1_1_7/utils.py
rename to trains/community/immich/1.7.25/templates/library/base_v1_1_7/utils.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/__init__.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/__init__.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/configs.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/configs.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/container.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/container.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/depends.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/depends.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deploy.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deploy.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_perms.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_redis.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/device.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/device.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/devices.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/devices.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/dns.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/dns.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/environment.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/environment.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/error.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/error.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/expose.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/expose.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/formatter.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/formatter.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/functions.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/functions.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/healthcheck.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/labels.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/labels.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/notes.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/notes.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/portal.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/portal.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/portals.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/portals.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/ports.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/ports.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/render.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/render.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/resources.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/resources.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/restart.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/restart.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/storage.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/storage.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/sysctls.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/validations.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/validations.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_mount.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_sources.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_types.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volumes.py b/trains/community/immich/1.7.25/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/volumes.py
rename to trains/community/immich/1.7.25/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/immich/1.7.24/templates/test_values/basic-values.yaml b/trains/community/immich/1.7.25/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/immich/1.7.24/templates/test_values/basic-values.yaml
rename to trains/community/immich/1.7.25/templates/test_values/basic-values.yaml
diff --git a/trains/community/immich/app_versions.json b/trains/community/immich/app_versions.json
index 0b951a00bc..a8b1e7aec7 100644
--- a/trains/community/immich/app_versions.json
+++ b/trains/community/immich/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.7.24": {
+ "1.7.25": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/immich/1.7.24",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/immich/1.7.25",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "v1.125.6_1.7.24",
- "version": "1.7.24",
+ "human_version": "v1.125.7_1.7.25",
+ "version": "1.7.25",
"app_metadata": {
- "app_version": "v1.125.6",
+ "app_version": "v1.125.7",
"capabilities": [
{
"description": "Immich Proxy is able to chown files.",
@@ -75,7 +75,7 @@
],
"title": "Immich",
"train": "community",
- "version": "1.7.24"
+ "version": "1.7.25"
},
"schema": {
"groups": [
diff --git a/trains/community/invidious/app_versions.json b/trains/community/invidious/app_versions.json
index fe7bcd838f..bada88f2de 100644
--- a/trains/community/invidious/app_versions.json
+++ b/trains/community/invidious/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/invidious/1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.20241110.0_1.2.8",
"version": "1.2.8",
diff --git a/trains/community/invoice-ninja/app_versions.json b/trains/community/invoice-ninja/app_versions.json
index a7d38b3ea8..e9063c76e9 100644
--- a/trains/community/invoice-ninja/app_versions.json
+++ b/trains/community/invoice-ninja/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/invoice-ninja/1.0.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "5.11.33_1.0.14",
"version": "1.0.14",
diff --git a/trains/community/ipfs/1.1.8/README.md b/trains/community/ipfs/1.1.9/README.md
similarity index 100%
rename from trains/community/ipfs/1.1.8/README.md
rename to trains/community/ipfs/1.1.9/README.md
diff --git a/trains/community/ipfs/1.1.8/app.yaml b/trains/community/ipfs/1.1.9/app.yaml
similarity index 96%
rename from trains/community/ipfs/1.1.8/app.yaml
rename to trains/community/ipfs/1.1.9/app.yaml
index 644cf63305..f2eff323b8 100644
--- a/trains/community/ipfs/1.1.8/app.yaml
+++ b/trains/community/ipfs/1.1.9/app.yaml
@@ -1,4 +1,4 @@
-app_version: v0.32.1
+app_version: v0.33.0
capabilities: []
categories:
- storage
@@ -33,4 +33,4 @@ sources:
- https://ipfs.tech/
title: IPFS
train: community
-version: 1.1.8
+version: 1.1.9
diff --git a/trains/community/ipfs/1.1.8/ix_values.yaml b/trains/community/ipfs/1.1.9/ix_values.yaml
similarity index 90%
rename from trains/community/ipfs/1.1.8/ix_values.yaml
rename to trains/community/ipfs/1.1.9/ix_values.yaml
index 738a79e13b..906b89b42d 100644
--- a/trains/community/ipfs/1.1.8/ix_values.yaml
+++ b/trains/community/ipfs/1.1.9/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ipfs/kubo
- tag: v0.32.1
+ tag: v0.33.0
consts:
ipfs_container_name: ipfs
diff --git a/trains/community/ipfs/1.1.8/migrations/migrate_from_kubernetes b/trains/community/ipfs/1.1.9/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migrate_from_kubernetes
rename to trains/community/ipfs/1.1.9/migrations/migrate_from_kubernetes
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/__init__.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/__init__.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/__init__.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/cpu.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/cpu.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/cpu.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/dns_config.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/dns_config.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/memory.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/memory.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/memory.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/resources.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/resources.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/resources.py
diff --git a/trains/community/ipfs/1.1.8/migrations/migration_helpers/storage.py b/trains/community/ipfs/1.1.9/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/ipfs/1.1.8/migrations/migration_helpers/storage.py
rename to trains/community/ipfs/1.1.9/migrations/migration_helpers/storage.py
diff --git a/trains/community/ipfs/1.1.8/questions.yaml b/trains/community/ipfs/1.1.9/questions.yaml
similarity index 100%
rename from trains/community/ipfs/1.1.8/questions.yaml
rename to trains/community/ipfs/1.1.9/questions.yaml
diff --git a/trains/community/ipfs/1.1.8/templates/docker-compose.yaml b/trains/community/ipfs/1.1.9/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/docker-compose.yaml
rename to trains/community/ipfs/1.1.9/templates/docker-compose.yaml
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/__init__.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/__init__.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/configs.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/configs.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/container.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/container.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/depends.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/depends.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deploy.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deploy.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_perms.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_redis.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/device.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/device.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/devices.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/devices.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/dns.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/dns.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/environment.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/environment.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/error.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/error.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/expose.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/expose.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/formatter.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/formatter.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/functions.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/functions.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/healthcheck.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/labels.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/labels.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/notes.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/notes.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/portal.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/portal.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/portals.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/portals.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/ports.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/ports.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/render.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/render.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/resources.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/resources.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/restart.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/restart.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/storage.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/storage.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/sysctls.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/__init__.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/__init__.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/validations.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/validations.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_mount.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_sources.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_types.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volumes.py b/trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/volumes.py
rename to trains/community/ipfs/1.1.9/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/ipfs/1.1.8/templates/macros/init.sh.jinja b/trains/community/ipfs/1.1.9/templates/macros/init.sh.jinja
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/macros/init.sh.jinja
rename to trains/community/ipfs/1.1.9/templates/macros/init.sh.jinja
diff --git a/trains/community/ipfs/1.1.8/templates/test_values/basic-values.yaml b/trains/community/ipfs/1.1.9/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/ipfs/1.1.8/templates/test_values/basic-values.yaml
rename to trains/community/ipfs/1.1.9/templates/test_values/basic-values.yaml
diff --git a/trains/community/ipfs/app_versions.json b/trains/community/ipfs/app_versions.json
index 7bd340fc02..05c6ff5305 100644
--- a/trains/community/ipfs/app_versions.json
+++ b/trains/community/ipfs/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.1.8": {
+ "1.1.9": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/ipfs/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/ipfs/1.1.9",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "v0.32.1_1.1.8",
- "version": "1.1.8",
+ "human_version": "v0.33.0_1.1.9",
+ "version": "1.1.9",
"app_metadata": {
- "app_version": "v0.32.1",
+ "app_version": "v0.33.0",
"capabilities": [],
"categories": [
"storage"
@@ -53,7 +53,7 @@
],
"title": "IPFS",
"train": "community",
- "version": "1.1.8"
+ "version": "1.1.9"
},
"schema": {
"groups": [
diff --git a/trains/community/it-tools/app_versions.json b/trains/community/it-tools/app_versions.json
index 9d21d23eac..13ab67bdef 100644
--- a/trains/community/it-tools/app_versions.json
+++ b/trains/community/it-tools/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/it-tools/1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2024.10.22_1.0.2",
"version": "1.0.2",
diff --git a/trains/community/jellyfin/app_versions.json b/trains/community/jellyfin/app_versions.json
index 4ae10f4da9..ce950b3299 100644
--- a/trains/community/jellyfin/app_versions.json
+++ b/trains/community/jellyfin/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/jellyfin/1.1.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "10.10.5_1.1.16",
"version": "1.1.16",
diff --git a/trains/community/jellyseerr/app_versions.json b/trains/community/jellyseerr/app_versions.json
index a11f429fc7..45d83781ea 100644
--- a/trains/community/jellyseerr/app_versions.json
+++ b/trains/community/jellyseerr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/jellyseerr/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.3.0_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/jelu/app_versions.json b/trains/community/jelu/app_versions.json
index a17c1f2ea6..caab5c3082 100644
--- a/trains/community/jelu/app_versions.json
+++ b/trains/community/jelu/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/jelu/1.0.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.66.0_1.0.9",
"version": "1.0.9",
diff --git a/trains/community/jenkins/app_versions.json b/trains/community/jenkins/app_versions.json
index 59b32e3767..070ae24da3 100644
--- a/trains/community/jenkins/app_versions.json
+++ b/trains/community/jenkins/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/jenkins/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.479.3-jdk17_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/joplin/app_versions.json b/trains/community/joplin/app_versions.json
index 47bc28be12..4d9ed4078f 100644
--- a/trains/community/joplin/app_versions.json
+++ b/trains/community/joplin/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/joplin/1.3.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "3.0.1-beta_1.3.6",
"version": "1.3.6",
diff --git a/trains/community/kapowarr/app_versions.json b/trains/community/kapowarr/app_versions.json
index dd6ebb4a65..2f93f989eb 100644
--- a/trains/community/kapowarr/app_versions.json
+++ b/trains/community/kapowarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/kapowarr/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.1.0_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/kavita/app_versions.json b/trains/community/kavita/app_versions.json
index c3da4009ed..b8ecb3e87e 100644
--- a/trains/community/kavita/app_versions.json
+++ b/trains/community/kavita/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/kavita/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.8.4_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/komga/app_versions.json b/trains/community/komga/app_versions.json
index 40619810e9..eee18da58d 100644
--- a/trains/community/komga/app_versions.json
+++ b/trains/community/komga/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/komga/1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.19.0_1.2.12",
"version": "1.2.12",
diff --git a/trains/community/lidarr/app_versions.json b/trains/community/lidarr/app_versions.json
index df07010f58..f0c5558bae 100644
--- a/trains/community/lidarr/app_versions.json
+++ b/trains/community/lidarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/lidarr/1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.9.4.4539_1.2.15",
"version": "1.2.15",
diff --git a/trains/community/linkding/app_versions.json b/trains/community/linkding/app_versions.json
index 2b74565ae4..b1f58a54ee 100644
--- a/trains/community/linkding/app_versions.json
+++ b/trains/community/linkding/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/linkding/1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.37.0_1.2.7",
"version": "1.2.7",
diff --git a/trains/community/listmonk/app_versions.json b/trains/community/listmonk/app_versions.json
index afdd930411..3d6ea26ffc 100644
--- a/trains/community/listmonk/app_versions.json
+++ b/trains/community/listmonk/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/listmonk/1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v4.1.0_1.2.6",
"version": "1.2.6",
diff --git a/trains/community/logseq/app_versions.json b/trains/community/logseq/app_versions.json
index fc76df9a87..99480e1f79 100644
--- a/trains/community/logseq/app_versions.json
+++ b/trains/community/logseq/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/logseq/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/lyrion-music-server/app_versions.json b/trains/community/lyrion-music-server/app_versions.json
index d095ccd8c3..338dec5ada 100644
--- a/trains/community/lyrion-music-server/app_versions.json
+++ b/trains/community/lyrion-music-server/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/lyrion-music-server/1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "9.1.0_1.0.6",
"version": "1.0.6",
diff --git a/trains/community/mealie/app_versions.json b/trains/community/mealie/app_versions.json
index 1b20d8e1ef..748f764913 100644
--- a/trains/community/mealie/app_versions.json
+++ b/trains/community/mealie/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/mealie/1.4.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v2.5.0_1.4.10",
"version": "1.4.10",
diff --git a/trains/community/metube/app_versions.json b/trains/community/metube/app_versions.json
index 652135a745..1821d39827 100644
--- a/trains/community/metube/app_versions.json
+++ b/trains/community/metube/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/metube/1.2.19",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2025-01-27_1.2.19",
"version": "1.2.19",
diff --git a/trains/community/minecraft-bedrock/app_versions.json b/trains/community/minecraft-bedrock/app_versions.json
index fe140a1d81..b658cae288 100644
--- a/trains/community/minecraft-bedrock/app_versions.json
+++ b/trains/community/minecraft-bedrock/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/minecraft-bedrock/1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2024.11.0_1.0.3",
"version": "1.0.3",
diff --git a/trains/community/minecraft/app_versions.json b/trains/community/minecraft/app_versions.json
index 2eb3c6662a..13d8e74d1d 100644
--- a/trains/community/minecraft/app_versions.json
+++ b/trains/community/minecraft/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/minecraft/1.12.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2025.1.0_1.12.10",
"version": "1.12.10",
diff --git a/trains/community/mineos/app_versions.json b/trains/community/mineos/app_versions.json
index 776888e947..8eb5ee3961 100644
--- a/trains/community/mineos/app_versions.json
+++ b/trains/community/mineos/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/mineos/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/mumble/app_versions.json b/trains/community/mumble/app_versions.json
index eb4b772d0e..0e9fa2c43f 100644
--- a/trains/community/mumble/app_versions.json
+++ b/trains/community/mumble/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/mumble/1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.5.735_1.2.8",
"version": "1.2.8",
diff --git a/trains/community/n8n/1.5.19/README.md b/trains/community/n8n/1.5.20/README.md
similarity index 100%
rename from trains/community/n8n/1.5.19/README.md
rename to trains/community/n8n/1.5.20/README.md
diff --git a/trains/community/n8n/1.5.19/app.yaml b/trains/community/n8n/1.5.20/app.yaml
similarity index 97%
rename from trains/community/n8n/1.5.19/app.yaml
rename to trains/community/n8n/1.5.20/app.yaml
index 0082813c0f..0f1606d594 100644
--- a/trains/community/n8n/1.5.19/app.yaml
+++ b/trains/community/n8n/1.5.20/app.yaml
@@ -1,4 +1,4 @@
-app_version: 1.76.1
+app_version: 1.77.0
capabilities: []
categories:
- productivity
@@ -42,4 +42,4 @@ sources:
- https://hub.docker.com/r/n8nio/n8n
title: n8n
train: community
-version: 1.5.19
+version: 1.5.20
diff --git a/trains/community/n8n/1.5.19/ix_values.yaml b/trains/community/n8n/1.5.20/ix_values.yaml
similarity index 96%
rename from trains/community/n8n/1.5.19/ix_values.yaml
rename to trains/community/n8n/1.5.20/ix_values.yaml
index 941bfa752b..efa6373876 100644
--- a/trains/community/n8n/1.5.19/ix_values.yaml
+++ b/trains/community/n8n/1.5.20/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: n8nio/n8n
- tag: "1.76.1"
+ tag: "1.77.0"
postgres_15_image:
repository: postgres
tag: "15.10"
diff --git a/trains/community/n8n/1.5.19/migrations/migrate_from_kubernetes b/trains/community/n8n/1.5.20/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migrate_from_kubernetes
rename to trains/community/n8n/1.5.20/migrations/migrate_from_kubernetes
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/__init__.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/cpu.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/cpu.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/cpu.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/dns_config.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/dns_config.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/memory.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/memory.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/memory.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/resources.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/resources.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/resources.py
diff --git a/trains/community/n8n/1.5.19/migrations/migration_helpers/storage.py b/trains/community/n8n/1.5.20/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/n8n/1.5.19/migrations/migration_helpers/storage.py
rename to trains/community/n8n/1.5.20/migrations/migration_helpers/storage.py
diff --git a/trains/community/n8n/1.5.19/questions.yaml b/trains/community/n8n/1.5.20/questions.yaml
similarity index 100%
rename from trains/community/n8n/1.5.19/questions.yaml
rename to trains/community/n8n/1.5.20/questions.yaml
diff --git a/trains/community/n8n/1.5.19/templates/docker-compose.yaml b/trains/community/n8n/1.5.20/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/docker-compose.yaml
rename to trains/community/n8n/1.5.20/templates/docker-compose.yaml
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/__init__.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/__init__.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/configs.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/configs.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/container.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/container.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/depends.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/depends.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deploy.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deploy.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_perms.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_redis.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/device.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/device.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/devices.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/devices.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/dns.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/dns.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/environment.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/environment.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/error.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/error.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/expose.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/expose.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/formatter.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/formatter.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/functions.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/functions.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/healthcheck.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/labels.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/labels.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/notes.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/notes.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/portal.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/portal.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/portals.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/portals.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/ports.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/ports.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/render.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/render.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/resources.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/resources.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/restart.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/restart.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/storage.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/storage.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/sysctls.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/__init__.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/__init__.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/validations.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/validations.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_mount.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_sources.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_types.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/volumes.py b/trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/volumes.py
rename to trains/community/n8n/1.5.20/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/n8n/1.5.19/templates/test_values/basic-values.yaml b/trains/community/n8n/1.5.20/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/test_values/basic-values.yaml
rename to trains/community/n8n/1.5.20/templates/test_values/basic-values.yaml
diff --git a/trains/community/n8n/1.5.19/templates/test_values/https-values.yaml b/trains/community/n8n/1.5.20/templates/test_values/https-values.yaml
similarity index 100%
rename from trains/community/n8n/1.5.19/templates/test_values/https-values.yaml
rename to trains/community/n8n/1.5.20/templates/test_values/https-values.yaml
diff --git a/trains/community/n8n/app_versions.json b/trains/community/n8n/app_versions.json
index dc6073b305..16a7de0269 100644
--- a/trains/community/n8n/app_versions.json
+++ b/trains/community/n8n/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.5.19": {
+ "1.5.20": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/n8n/1.5.19",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/n8n/1.5.20",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "1.76.1_1.5.19",
- "version": "1.5.19",
+ "human_version": "1.77.0_1.5.20",
+ "version": "1.5.20",
"app_metadata": {
- "app_version": "1.76.1",
+ "app_version": "1.77.0",
"capabilities": [],
"categories": [
"productivity"
@@ -67,7 +67,7 @@
],
"title": "n8n",
"train": "community",
- "version": "1.5.19"
+ "version": "1.5.20"
},
"schema": {
"groups": [
diff --git a/trains/community/navidrome/app_versions.json b/trains/community/navidrome/app_versions.json
index a4ef900466..e1f8ae0577 100644
--- a/trains/community/navidrome/app_versions.json
+++ b/trains/community/navidrome/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/navidrome/1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.54.4_1.1.10",
"version": "1.1.10",
diff --git a/trains/community/netbootxyz/app_versions.json b/trains/community/netbootxyz/app_versions.json
index fb07b5c3d2..c405ceb1f9 100644
--- a/trains/community/netbootxyz/app_versions.json
+++ b/trains/community/netbootxyz/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/netbootxyz/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.7.3-nbxyz1_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/nginx-proxy-manager/app_versions.json b/trains/community/nginx-proxy-manager/app_versions.json
index 2c14061cea..7d1cf41cb3 100644
--- a/trains/community/nginx-proxy-manager/app_versions.json
+++ b/trains/community/nginx-proxy-manager/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/nginx-proxy-manager/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.12.2_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/node-red/app_versions.json b/trains/community/node-red/app_versions.json
index 07bd8789f7..d0cbd1962c 100644
--- a/trains/community/node-red/app_versions.json
+++ b/trains/community/node-red/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/node-red/1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "4.0.8_1.1.10",
"version": "1.1.10",
diff --git a/trains/community/odoo/app_versions.json b/trains/community/odoo/app_versions.json
index c88be6ba70..ca64b3e910 100644
--- a/trains/community/odoo/app_versions.json
+++ b/trains/community/odoo/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/odoo/1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "17.0_1.2.6",
"version": "1.2.6",
diff --git a/trains/community/ollama/app_versions.json b/trains/community/ollama/app_versions.json
index aaba64d042..2c237469a5 100644
--- a/trains/community/ollama/app_versions.json
+++ b/trains/community/ollama/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/ollama/1.0.29",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.5.7_1.0.29",
"version": "1.0.29",
diff --git a/trains/community/omada-controller/app_versions.json b/trains/community/omada-controller/app_versions.json
index 3cf5ce043c..00b81ec662 100644
--- a/trains/community/omada-controller/app_versions.json
+++ b/trains/community/omada-controller/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/omada-controller/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "5.15_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/open-webui/app_versions.json b/trains/community/open-webui/app_versions.json
index e720c0f2c3..2fe23e2508 100644
--- a/trains/community/open-webui/app_versions.json
+++ b/trains/community/open-webui/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/open-webui/1.0.26",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.5.7_1.0.26",
"version": "1.0.26",
diff --git a/trains/community/organizr/app_versions.json b/trains/community/organizr/app_versions.json
index 5747fb43cf..55a76c78d4 100644
--- a/trains/community/organizr/app_versions.json
+++ b/trains/community/organizr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/organizr/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/overseerr/app_versions.json b/trains/community/overseerr/app_versions.json
index dccf06a0e6..6f062d7174 100644
--- a/trains/community/overseerr/app_versions.json
+++ b/trains/community/overseerr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/overseerr/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.33.2_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/palworld/app_versions.json b/trains/community/palworld/app_versions.json
index fe3fbdf425..66da28b218 100644
--- a/trains/community/palworld/app_versions.json
+++ b/trains/community/palworld/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/palworld/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "palworld_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/paperless-ngx/1.2.19/README.md b/trains/community/paperless-ngx/1.2.20/README.md
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/README.md
rename to trains/community/paperless-ngx/1.2.20/README.md
diff --git a/trains/community/paperless-ngx/1.2.19/app.yaml b/trains/community/paperless-ngx/1.2.20/app.yaml
similarity index 99%
rename from trains/community/paperless-ngx/1.2.19/app.yaml
rename to trains/community/paperless-ngx/1.2.20/app.yaml
index 55c38b5c7a..cf59b5fa4a 100644
--- a/trains/community/paperless-ngx/1.2.19/app.yaml
+++ b/trains/community/paperless-ngx/1.2.20/app.yaml
@@ -67,4 +67,4 @@ sources:
- https://github.com/paperless-ngx/paperless-ngx
title: Paperless-ngx
train: community
-version: 1.2.19
+version: 1.2.20
diff --git a/trains/community/paperless-ngx/1.2.19/ix_values.yaml b/trains/community/paperless-ngx/1.2.20/ix_values.yaml
similarity index 98%
rename from trains/community/paperless-ngx/1.2.19/ix_values.yaml
rename to trains/community/paperless-ngx/1.2.20/ix_values.yaml
index 49010b976d..c4e780f742 100644
--- a/trains/community/paperless-ngx/1.2.19/ix_values.yaml
+++ b/trains/community/paperless-ngx/1.2.20/ix_values.yaml
@@ -16,7 +16,7 @@ images:
tag: "3.0.0.0-full"
gotenberg_image:
repository: gotenberg/gotenberg
- tag: "8.15.3"
+ tag: "8.16.0"
consts:
paperless_container_name: paperless
perms_container_name: permissions
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migrate_from_kubernetes b/trains/community/paperless-ngx/1.2.20/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migrate_from_kubernetes
rename to trains/community/paperless-ngx/1.2.20/migrations/migrate_from_kubernetes
diff --git a/trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/__init__.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/cpu.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/cpu.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/cpu.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/dns_config.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/dns_config.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/memory.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/memory.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/memory.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/resources.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/resources.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/resources.py
diff --git a/trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/storage.py b/trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/migrations/migration_helpers/storage.py
rename to trains/community/paperless-ngx/1.2.20/migrations/migration_helpers/storage.py
diff --git a/trains/community/paperless-ngx/1.2.19/questions.yaml b/trains/community/paperless-ngx/1.2.20/questions.yaml
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/questions.yaml
rename to trains/community/paperless-ngx/1.2.20/questions.yaml
diff --git a/trains/community/paperless-ngx/1.2.19/templates/docker-compose.yaml b/trains/community/paperless-ngx/1.2.20/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/docker-compose.yaml
rename to trains/community/paperless-ngx/1.2.20/templates/docker-compose.yaml
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/__init__.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/__init__.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/__init__.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/__init__.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/environment.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/environment.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/environment.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/environment.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/healthchecks.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/healthchecks.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/healthchecks.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/healthchecks.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/mariadb.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/mariadb.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/mariadb.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/mariadb.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/metadata.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/metadata.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/metadata.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/metadata.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/network.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/network.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/network.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/network.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/permissions.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/permissions.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/permissions.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/permissions.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/ports.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/ports.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/ports.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/ports.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/postgres.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/postgres.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/postgres.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/postgres.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/redis.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/redis.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/redis.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/redis.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/resources.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/resources.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/resources.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/resources.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/security.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/security.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/security.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/security.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/storage.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/storage.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/storage.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/storage.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/utils.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/utils.py
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/library/base_v1_1_7/utils.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v1_1_7/utils.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/__init__.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/__init__.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/configs.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/configs.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/container.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/container.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/depends.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/depends.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deploy.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deploy.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_perms.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_redis.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/device.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/device.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/devices.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/devices.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/dns.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/dns.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/environment.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/environment.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/error.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/error.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/expose.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/expose.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/formatter.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/formatter.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/functions.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/functions.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/healthcheck.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/labels.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/labels.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/notes.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/notes.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/portal.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/portal.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/portals.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/portals.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/ports.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/ports.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/render.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/render.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/resources.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/resources.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/restart.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/restart.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/storage.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/storage.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/sysctls.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/planka/1.2.6/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/validations.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/validations.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_mount.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_sources.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_types.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volumes.py b/trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/volumes.py
rename to trains/community/paperless-ngx/1.2.20/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/paperless-ngx/1.2.19/templates/test_values/basic-values.yaml b/trains/community/paperless-ngx/1.2.20/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/test_values/basic-values.yaml
rename to trains/community/paperless-ngx/1.2.20/templates/test_values/basic-values.yaml
diff --git a/trains/community/paperless-ngx/1.2.19/templates/test_values/tika-gotenberg-values.yaml b/trains/community/paperless-ngx/1.2.20/templates/test_values/tika-gotenberg-values.yaml
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/test_values/tika-gotenberg-values.yaml
rename to trains/community/paperless-ngx/1.2.20/templates/test_values/tika-gotenberg-values.yaml
diff --git a/trains/community/paperless-ngx/1.2.19/templates/test_values/trash-values.yaml b/trains/community/paperless-ngx/1.2.20/templates/test_values/trash-values.yaml
similarity index 100%
rename from trains/community/paperless-ngx/1.2.19/templates/test_values/trash-values.yaml
rename to trains/community/paperless-ngx/1.2.20/templates/test_values/trash-values.yaml
diff --git a/trains/community/paperless-ngx/app_versions.json b/trains/community/paperless-ngx/app_versions.json
index dd932eb96d..33f4ef6872 100644
--- a/trains/community/paperless-ngx/app_versions.json
+++ b/trains/community/paperless-ngx/app_versions.json
@@ -1,13 +1,13 @@
{
- "1.2.19": {
+ "1.2.20": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/paperless-ngx/1.2.19",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/paperless-ngx/1.2.20",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "2.14.6_1.2.19",
- "version": "1.2.19",
+ "human_version": "2.14.6_1.2.20",
+ "version": "1.2.20",
"app_metadata": {
"app_version": "2.14.6",
"capabilities": [
@@ -106,7 +106,7 @@
],
"title": "Paperless-ngx",
"train": "community",
- "version": "1.2.19"
+ "version": "1.2.20"
},
"schema": {
"groups": [
diff --git a/trains/community/passbolt/1.1.7/README.md b/trains/community/passbolt/1.1.8/README.md
similarity index 100%
rename from trains/community/passbolt/1.1.7/README.md
rename to trains/community/passbolt/1.1.8/README.md
diff --git a/trains/community/passbolt/1.1.7/app.yaml b/trains/community/passbolt/1.1.8/app.yaml
similarity index 96%
rename from trains/community/passbolt/1.1.7/app.yaml
rename to trains/community/passbolt/1.1.8/app.yaml
index b8b5b8e3b4..3eb21f7c90 100644
--- a/trains/community/passbolt/1.1.7/app.yaml
+++ b/trains/community/passbolt/1.1.8/app.yaml
@@ -1,4 +1,4 @@
-app_version: 4.10.1
+app_version: 4.11.0
capabilities: []
categories:
- security
@@ -37,4 +37,4 @@ sources:
- https://www.passbolt.com
title: Passbolt
train: community
-version: 1.1.7
+version: 1.1.8
diff --git a/trains/community/passbolt/1.1.7/ix_values.yaml b/trains/community/passbolt/1.1.8/ix_values.yaml
similarity index 96%
rename from trains/community/passbolt/1.1.7/ix_values.yaml
rename to trains/community/passbolt/1.1.8/ix_values.yaml
index 90cbc810dd..6676c1aee9 100644
--- a/trains/community/passbolt/1.1.7/ix_values.yaml
+++ b/trains/community/passbolt/1.1.8/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: passbolt/passbolt
- tag: 4.10.1-1-ce-non-root
+ tag: 4.11.0-1-ce-non-root
mariadb_image:
repository: mariadb
tag: "10.11.10"
diff --git a/trains/community/passbolt/1.1.7/migrations/migrate_from_kubernetes b/trains/community/passbolt/1.1.8/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migrate_from_kubernetes
rename to trains/community/passbolt/1.1.8/migrations/migrate_from_kubernetes
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/__init__.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/__init__.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/__init__.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/cpu.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/cpu.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/cpu.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/dns_config.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/dns_config.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/memory.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/memory.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/memory.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/resources.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/resources.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/resources.py
diff --git a/trains/community/passbolt/1.1.7/migrations/migration_helpers/storage.py b/trains/community/passbolt/1.1.8/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/passbolt/1.1.7/migrations/migration_helpers/storage.py
rename to trains/community/passbolt/1.1.8/migrations/migration_helpers/storage.py
diff --git a/trains/community/passbolt/1.1.7/questions.yaml b/trains/community/passbolt/1.1.8/questions.yaml
similarity index 100%
rename from trains/community/passbolt/1.1.7/questions.yaml
rename to trains/community/passbolt/1.1.8/questions.yaml
diff --git a/trains/community/passbolt/1.1.7/templates/docker-compose.yaml b/trains/community/passbolt/1.1.8/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/docker-compose.yaml
rename to trains/community/passbolt/1.1.8/templates/docker-compose.yaml
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/__init__.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/__init__.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/configs.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/configs.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/container.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/container.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/depends.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/depends.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deploy.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deploy.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_perms.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_redis.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/device.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/device.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/devices.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/devices.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/dns.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/dns.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/environment.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/environment.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/error.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/error.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/expose.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/expose.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/formatter.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/formatter.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/functions.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/functions.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/healthcheck.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/labels.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/labels.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/notes.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/notes.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/portal.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/portal.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/portals.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/portals.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/ports.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/ports.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/render.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/render.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/resources.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/resources.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/restart.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/restart.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/storage.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/storage.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/sysctls.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/validations.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/validations.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_mount.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_sources.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_types.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volumes.py b/trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/volumes.py
rename to trains/community/passbolt/1.1.8/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/passbolt/1.1.7/templates/test_values/basic-values.yaml b/trains/community/passbolt/1.1.8/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/test_values/basic-values.yaml
rename to trains/community/passbolt/1.1.8/templates/test_values/basic-values.yaml
diff --git a/trains/community/passbolt/1.1.7/templates/test_values/https-values.yaml b/trains/community/passbolt/1.1.8/templates/test_values/https-values.yaml
similarity index 100%
rename from trains/community/passbolt/1.1.7/templates/test_values/https-values.yaml
rename to trains/community/passbolt/1.1.8/templates/test_values/https-values.yaml
diff --git a/trains/community/passbolt/app_versions.json b/trains/community/passbolt/app_versions.json
index 30d0d4b0cc..e7b0e6cb78 100644
--- a/trains/community/passbolt/app_versions.json
+++ b/trains/community/passbolt/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.1.7": {
+ "1.1.8": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/passbolt/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/passbolt/1.1.8",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "4.10.1_1.1.7",
- "version": "1.1.7",
+ "human_version": "4.11.0_1.1.8",
+ "version": "1.1.8",
"app_metadata": {
- "app_version": "4.10.1",
+ "app_version": "4.11.0",
"capabilities": [],
"categories": [
"security"
@@ -60,7 +60,7 @@
],
"title": "Passbolt",
"train": "community",
- "version": "1.1.7"
+ "version": "1.1.8"
},
"schema": {
"groups": [
diff --git a/trains/community/penpot/app_versions.json b/trains/community/penpot/app_versions.json
index 3ae6185b77..792058b5ba 100644
--- a/trains/community/penpot/app_versions.json
+++ b/trains/community/penpot/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/penpot/1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.4.2_1.1.11",
"version": "1.1.11",
diff --git a/trains/community/pgadmin/app_versions.json b/trains/community/pgadmin/app_versions.json
index 9b43569d3f..84bd4c2270 100644
--- a/trains/community/pgadmin/app_versions.json
+++ b/trains/community/pgadmin/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/pgadmin/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "8.14_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/pigallery2/app_versions.json b/trains/community/pigallery2/app_versions.json
index 46379a11dc..ade6d937b4 100644
--- a/trains/community/pigallery2/app_versions.json
+++ b/trains/community/pigallery2/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/pigallery2/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.0.0_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/piwigo/app_versions.json b/trains/community/piwigo/app_versions.json
index 413d68b5aa..8159f8bd2c 100644
--- a/trains/community/piwigo/app_versions.json
+++ b/trains/community/piwigo/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/piwigo/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "15.3.0_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/planka/1.2.6/README.md b/trains/community/planka/1.2.7/README.md
similarity index 100%
rename from trains/community/planka/1.2.6/README.md
rename to trains/community/planka/1.2.7/README.md
diff --git a/trains/community/planka/1.2.6/app.yaml b/trains/community/planka/1.2.7/app.yaml
similarity index 96%
rename from trains/community/planka/1.2.6/app.yaml
rename to trains/community/planka/1.2.7/app.yaml
index 43b3ab2b05..0d738518f0 100644
--- a/trains/community/planka/1.2.6/app.yaml
+++ b/trains/community/planka/1.2.7/app.yaml
@@ -1,4 +1,4 @@
-app_version: 1.24.3
+app_version: 1.24.4
capabilities: []
categories:
- productivity
@@ -35,4 +35,4 @@ sources:
- https://github.com/plankanban/planka
title: Planka
train: community
-version: 1.2.6
+version: 1.2.7
diff --git a/trains/community/planka/1.2.6/ix_values.yaml b/trains/community/planka/1.2.7/ix_values.yaml
similarity index 95%
rename from trains/community/planka/1.2.6/ix_values.yaml
rename to trains/community/planka/1.2.7/ix_values.yaml
index d115ebdb11..ba82358769 100644
--- a/trains/community/planka/1.2.6/ix_values.yaml
+++ b/trains/community/planka/1.2.7/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: ghcr.io/plankanban/planka
- tag: "1.24.3"
+ tag: "1.24.4"
postgres_15_image:
repository: postgres
tag: "15.10"
diff --git a/trains/community/planka/1.2.6/migrations/migrate_from_kubernetes b/trains/community/planka/1.2.7/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migrate_from_kubernetes
rename to trains/community/planka/1.2.7/migrations/migrate_from_kubernetes
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/__init__.py b/trains/community/planka/1.2.7/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/__init__.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/__init__.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/cpu.py b/trains/community/planka/1.2.7/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/cpu.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/cpu.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/dns_config.py b/trains/community/planka/1.2.7/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/dns_config.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/planka/1.2.7/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/memory.py b/trains/community/planka/1.2.7/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/memory.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/memory.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/resources.py b/trains/community/planka/1.2.7/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/resources.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/resources.py
diff --git a/trains/community/planka/1.2.6/migrations/migration_helpers/storage.py b/trains/community/planka/1.2.7/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/planka/1.2.6/migrations/migration_helpers/storage.py
rename to trains/community/planka/1.2.7/migrations/migration_helpers/storage.py
diff --git a/trains/community/planka/1.2.6/questions.yaml b/trains/community/planka/1.2.7/questions.yaml
similarity index 100%
rename from trains/community/planka/1.2.6/questions.yaml
rename to trains/community/planka/1.2.7/questions.yaml
diff --git a/trains/community/planka/1.2.6/templates/docker-compose.yaml b/trains/community/planka/1.2.7/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/planka/1.2.6/templates/docker-compose.yaml
rename to trains/community/planka/1.2.7/templates/docker-compose.yaml
diff --git a/trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/configs.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/configs.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/configs.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/container.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/container.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/container.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/depends.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/depends.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/depends.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deploy.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deploy.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_perms.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_redis.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/device.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/device.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/device.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/devices.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/devices.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/devices.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/dns.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/dns.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/dns.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/environment.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/environment.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/environment.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/error.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/error.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/error.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/expose.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/expose.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/expose.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/formatter.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/formatter.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/functions.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/functions.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/functions.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/healthcheck.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/labels.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/labels.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/labels.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/notes.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/notes.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/notes.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/portal.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/portal.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/portal.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/portals.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/portals.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/portals.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/ports.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/ports.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/ports.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/render.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/render.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/render.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/resources.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/resources.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/resources.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/restart.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/restart.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/restart.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/storage.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/storage.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/storage.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/sysctls.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/__init__.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/__init__.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/validations.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/validations.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/validations.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_mount.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_sources.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_types.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volumes.py b/trains/community/planka/1.2.7/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/volumes.py
rename to trains/community/planka/1.2.7/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/planka/1.2.6/templates/test_values/basic-values.yaml b/trains/community/planka/1.2.7/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/planka/1.2.6/templates/test_values/basic-values.yaml
rename to trains/community/planka/1.2.7/templates/test_values/basic-values.yaml
diff --git a/trains/community/planka/app_versions.json b/trains/community/planka/app_versions.json
index 1999a030cf..1f39e7f36a 100644
--- a/trains/community/planka/app_versions.json
+++ b/trains/community/planka/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.2.6": {
+ "1.2.7": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/planka/1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/planka/1.2.7",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "1.24.3_1.2.6",
- "version": "1.2.6",
+ "human_version": "1.24.4_1.2.7",
+ "version": "1.2.7",
"app_metadata": {
- "app_version": "1.24.3",
+ "app_version": "1.24.4",
"capabilities": [],
"categories": [
"productivity"
@@ -58,7 +58,7 @@
],
"title": "Planka",
"train": "community",
- "version": "1.2.6"
+ "version": "1.2.7"
},
"schema": {
"groups": [
diff --git a/trains/community/plex-auto-languages/app_versions.json b/trains/community/plex-auto-languages/app_versions.json
index 111cb2bf81..c0d8aeeef0 100644
--- a/trains/community/plex-auto-languages/app_versions.json
+++ b/trains/community/plex-auto-languages/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/plex-auto-languages/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.3.2_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/portainer/app_versions.json b/trains/community/portainer/app_versions.json
index 587a53ccca..dcd8a27549 100644
--- a/trains/community/portainer/app_versions.json
+++ b/trains/community/portainer/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/portainer/1.3.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.26.1_1.3.15",
"version": "1.3.15",
diff --git a/trains/community/postgres/app_versions.json b/trains/community/postgres/app_versions.json
index a6021797f4..1f2b965bb5 100644
--- a/trains/community/postgres/app_versions.json
+++ b/trains/community/postgres/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/postgres/1.0.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "17.2_1.0.15",
"version": "1.0.15",
diff --git a/trains/community/prowlarr/app_versions.json b/trains/community/prowlarr/app_versions.json
index 9fa1128315..b65aef97b8 100644
--- a/trains/community/prowlarr/app_versions.json
+++ b/trains/community/prowlarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/prowlarr/1.3.18",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.30.2.4939_1.3.18",
"version": "1.3.18",
diff --git a/trains/community/qbittorrent/app_versions.json b/trains/community/qbittorrent/app_versions.json
index 72dc6d414a..a99fec362f 100644
--- a/trains/community/qbittorrent/app_versions.json
+++ b/trains/community/qbittorrent/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/qbittorrent/1.1.16",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "5.0.3_1.1.16",
"version": "1.1.16",
diff --git a/trains/community/radarr/app_versions.json b/trains/community/radarr/app_versions.json
index 53d88fbd3c..f02f7fe2b4 100644
--- a/trains/community/radarr/app_versions.json
+++ b/trains/community/radarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/radarr/1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "5.17.2.9580_1.2.11",
"version": "1.2.11",
diff --git a/trains/community/readarr/app_versions.json b/trains/community/readarr/app_versions.json
index 433464499d..2602f2cf3f 100644
--- a/trains/community/readarr/app_versions.json
+++ b/trains/community/readarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/readarr/1.1.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.4.10.2734_1.1.15",
"version": "1.1.15",
diff --git a/trains/community/recyclarr/app_versions.json b/trains/community/recyclarr/app_versions.json
index f5192ebd55..d76609d7be 100644
--- a/trains/community/recyclarr/app_versions.json
+++ b/trains/community/recyclarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/recyclarr/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "7.4.0_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/redis/app_versions.json b/trains/community/redis/app_versions.json
index 7c3bd653e3..428e915fcc 100644
--- a/trains/community/redis/app_versions.json
+++ b/trains/community/redis/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/redis/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "7.4.2_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/romm/app_versions.json b/trains/community/romm/app_versions.json
index 429019c93c..7097657a6a 100644
--- a/trains/community/romm/app_versions.json
+++ b/trains/community/romm/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/romm/1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "3.7.3_1.0.4",
"version": "1.0.4",
diff --git a/trains/community/roundcube/app_versions.json b/trains/community/roundcube/app_versions.json
index e93f1aa1b6..c59a6e7981 100644
--- a/trains/community/roundcube/app_versions.json
+++ b/trains/community/roundcube/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/roundcube/1.2.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.6.9-apache_1.2.6",
"version": "1.2.6",
diff --git a/trains/community/rsyncd/app_versions.json b/trains/community/rsyncd/app_versions.json
index e8bc04336f..68199373c7 100644
--- a/trains/community/rsyncd/app_versions.json
+++ b/trains/community/rsyncd/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/rsyncd/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.0.2_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/rust-desk/app_versions.json b/trains/community/rust-desk/app_versions.json
index f6b2ea3215..5165cc1cf9 100644
--- a/trains/community/rust-desk/app_versions.json
+++ b/trains/community/rust-desk/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/rust-desk/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.1.11-1_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/sabnzbd/app_versions.json b/trains/community/sabnzbd/app_versions.json
index 3e19dd83e1..acba6e5030 100644
--- a/trains/community/sabnzbd/app_versions.json
+++ b/trains/community/sabnzbd/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/sabnzbd/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "4.4.1_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/satisfactory-server/app_versions.json b/trains/community/satisfactory-server/app_versions.json
index cc9aa794fb..c3e04c12bd 100644
--- a/trains/community/satisfactory-server/app_versions.json
+++ b/trains/community/satisfactory-server/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/satisfactory-server/1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.9.5_1.0.4",
"version": "1.0.4",
diff --git a/trains/community/scrutiny/app_versions.json b/trains/community/scrutiny/app_versions.json
index 33778584ad..8359dbb401 100644
--- a/trains/community/scrutiny/app_versions.json
+++ b/trains/community/scrutiny/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/scrutiny/1.0.13",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v0.8.1_1.0.13",
"version": "1.0.13",
diff --git a/trains/community/searxng/1.1.24/README.md b/trains/community/searxng/1.1.25/README.md
similarity index 100%
rename from trains/community/searxng/1.1.24/README.md
rename to trains/community/searxng/1.1.25/README.md
diff --git a/trains/community/searxng/1.1.24/app.yaml b/trains/community/searxng/1.1.25/app.yaml
similarity index 94%
rename from trains/community/searxng/1.1.24/app.yaml
rename to trains/community/searxng/1.1.25/app.yaml
index ce4f637981..625ea7b978 100644
--- a/trains/community/searxng/1.1.24/app.yaml
+++ b/trains/community/searxng/1.1.25/app.yaml
@@ -1,4 +1,4 @@
-app_version: 2025.1.29-fc8938c96
+app_version: 2025.1.31-eea4d4fd1
capabilities:
- description: SearXNG requires this ability to switch user for sub-processes.
name: SETUID
@@ -31,4 +31,4 @@ sources:
- https://github.com/searxng/searxng
title: SearXNG
train: community
-version: 1.1.24
+version: 1.1.25
diff --git a/trains/community/searxng/1.1.24/ix_values.yaml b/trains/community/searxng/1.1.25/ix_values.yaml
similarity index 81%
rename from trains/community/searxng/1.1.24/ix_values.yaml
rename to trains/community/searxng/1.1.25/ix_values.yaml
index 14b1680bb2..041af35e9c 100644
--- a/trains/community/searxng/1.1.24/ix_values.yaml
+++ b/trains/community/searxng/1.1.25/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: searxng/searxng
- tag: 2025.1.29-fc8938c96
+ tag: 2025.1.31-eea4d4fd1
consts:
searxng_container_name: searxng
diff --git a/trains/community/searxng/1.1.24/migrations/migrate_from_kubernetes b/trains/community/searxng/1.1.25/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migrate_from_kubernetes
rename to trains/community/searxng/1.1.25/migrations/migrate_from_kubernetes
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/__init__.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/__init__.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/__init__.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/cpu.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/cpu.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/cpu.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/dns_config.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/dns_config.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/memory.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/memory.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/memory.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/resources.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/resources.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/resources.py
diff --git a/trains/community/searxng/1.1.24/migrations/migration_helpers/storage.py b/trains/community/searxng/1.1.25/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/searxng/1.1.24/migrations/migration_helpers/storage.py
rename to trains/community/searxng/1.1.25/migrations/migration_helpers/storage.py
diff --git a/trains/community/searxng/1.1.24/questions.yaml b/trains/community/searxng/1.1.25/questions.yaml
similarity index 100%
rename from trains/community/searxng/1.1.24/questions.yaml
rename to trains/community/searxng/1.1.25/questions.yaml
diff --git a/trains/community/searxng/1.1.24/templates/docker-compose.yaml b/trains/community/searxng/1.1.25/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/docker-compose.yaml
rename to trains/community/searxng/1.1.25/templates/docker-compose.yaml
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/configs.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/configs.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/configs.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/container.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/container.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/container.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/depends.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/depends.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/depends.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deploy.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deploy.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_perms.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_redis.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/device.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/device.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/device.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/devices.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/devices.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/devices.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/dns.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/dns.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/dns.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/environment.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/environment.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/environment.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/error.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/error.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/error.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/expose.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/expose.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/expose.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/formatter.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/formatter.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/functions.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/functions.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/functions.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/healthcheck.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/labels.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/labels.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/labels.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/notes.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/notes.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/notes.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/portal.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/portal.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/portal.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/portals.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/portals.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/portals.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/ports.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/ports.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/ports.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/render.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/render.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/render.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/resources.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/resources.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/resources.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/restart.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/restart.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/restart.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/storage.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/storage.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/storage.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/sysctls.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/__init__.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/__init__.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/validations.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/validations.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/validations.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_mount.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_sources.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_types.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volumes.py b/trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/volumes.py
rename to trains/community/searxng/1.1.25/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/searxng/1.1.24/templates/test_values/basic-values.yaml b/trains/community/searxng/1.1.25/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/searxng/1.1.24/templates/test_values/basic-values.yaml
rename to trains/community/searxng/1.1.25/templates/test_values/basic-values.yaml
diff --git a/trains/community/searxng/app_versions.json b/trains/community/searxng/app_versions.json
index c6ba21a744..e59a090a3e 100644
--- a/trains/community/searxng/app_versions.json
+++ b/trains/community/searxng/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.1.24": {
+ "1.1.25": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/searxng/1.1.24",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/searxng/1.1.25",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "2025.1.29-fc8938c96_1.1.24",
- "version": "1.1.24",
+ "human_version": "2025.1.31-eea4d4fd1_1.1.25",
+ "version": "1.1.25",
"app_metadata": {
- "app_version": "2025.1.29-fc8938c96",
+ "app_version": "2025.1.31-eea4d4fd1",
"capabilities": [
{
"description": "SearXNG requires this ability to switch user for sub-processes.",
@@ -56,7 +56,7 @@
],
"title": "SearXNG",
"train": "community",
- "version": "1.1.24"
+ "version": "1.1.25"
},
"schema": {
"groups": [
diff --git a/trains/community/sftpgo/app_versions.json b/trains/community/sftpgo/app_versions.json
index 4124732651..d5f95d5eba 100644
--- a/trains/community/sftpgo/app_versions.json
+++ b/trains/community/sftpgo/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/sftpgo/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v2.6.4_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/sonarr/app_versions.json b/trains/community/sonarr/app_versions.json
index f5ddbd0f91..924a672e91 100644
--- a/trains/community/sonarr/app_versions.json
+++ b/trains/community/sonarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/sonarr/1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "4.0.12.2823_1.1.10",
"version": "1.1.10",
diff --git a/trains/community/steam-headless/1.0.4/README.md b/trains/community/steam-headless/1.0.5/README.md
similarity index 100%
rename from trains/community/steam-headless/1.0.4/README.md
rename to trains/community/steam-headless/1.0.5/README.md
diff --git a/trains/community/steam-headless/1.0.4/app.yaml b/trains/community/steam-headless/1.0.5/app.yaml
similarity index 99%
rename from trains/community/steam-headless/1.0.4/app.yaml
rename to trains/community/steam-headless/1.0.5/app.yaml
index 0cf59826f2..3ea0963b05 100644
--- a/trains/community/steam-headless/1.0.4/app.yaml
+++ b/trains/community/steam-headless/1.0.5/app.yaml
@@ -58,4 +58,4 @@ sources:
- https://github.com/Steam-Headless/docker-steam-headless
title: Steam Headless
train: community
-version: 1.0.4
+version: 1.0.5
diff --git a/trains/community/steam-headless/1.0.4/ix_values.yaml b/trains/community/steam-headless/1.0.5/ix_values.yaml
similarity index 100%
rename from trains/community/steam-headless/1.0.4/ix_values.yaml
rename to trains/community/steam-headless/1.0.5/ix_values.yaml
diff --git a/trains/community/steam-headless/1.0.4/questions.yaml b/trains/community/steam-headless/1.0.5/questions.yaml
similarity index 99%
rename from trains/community/steam-headless/1.0.4/questions.yaml
rename to trains/community/steam-headless/1.0.5/questions.yaml
index 03cfbff838..05b7ba5c18 100644
--- a/trains/community/steam-headless/1.0.4/questions.yaml
+++ b/trains/community/steam-headless/1.0.5/questions.yaml
@@ -754,3 +754,11 @@ questions:
type: int
default: 4096
required: true
+ - variable: gpus
+ group: Resources Configuration
+ label: GPU Configuration
+ schema:
+ type: dict
+ $ref:
+ - "definitions/gpu_configuration"
+ attrs: []
diff --git a/trains/community/steam-headless/1.0.4/templates/docker-compose.yaml b/trains/community/steam-headless/1.0.5/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/docker-compose.yaml
rename to trains/community/steam-headless/1.0.5/templates/docker-compose.yaml
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/configs.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/configs.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/configs.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/container.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/container.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/container.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/depends.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/depends.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/depends.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deploy.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deploy.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_perms.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_redis.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/device.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/device.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/device.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/devices.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/devices.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/devices.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/dns.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/dns.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/dns.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/environment.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/environment.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/environment.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/error.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/error.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/error.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/expose.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/expose.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/expose.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/formatter.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/formatter.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/functions.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/functions.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/functions.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/healthcheck.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/labels.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/labels.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/labels.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/notes.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/notes.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/notes.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/portal.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/portal.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/portal.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/portals.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/portals.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/portals.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/ports.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/ports.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/ports.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/render.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/render.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/render.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/resources.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/resources.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/resources.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/restart.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/restart.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/restart.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/storage.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/storage.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/storage.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/sysctls.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/__init__.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/__init__.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/validations.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/validations.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/validations.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_mount.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_sources.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_types.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volumes.py b/trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/volumes.py
rename to trains/community/steam-headless/1.0.5/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/steam-headless/1.0.4/templates/test_values/basic-values.yaml b/trains/community/steam-headless/1.0.5/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/steam-headless/1.0.4/templates/test_values/basic-values.yaml
rename to trains/community/steam-headless/1.0.5/templates/test_values/basic-values.yaml
diff --git a/trains/community/steam-headless/app_versions.json b/trains/community/steam-headless/app_versions.json
index e89d89334a..92b47b8af6 100644
--- a/trains/community/steam-headless/app_versions.json
+++ b/trains/community/steam-headless/app_versions.json
@@ -1,13 +1,13 @@
{
- "1.0.4": {
+ "1.0.5": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/steam-headless/1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/steam-headless/1.0.5",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "debian_1.0.4",
- "version": "1.0.4",
+ "human_version": "debian_1.0.5",
+ "version": "1.0.5",
"app_metadata": {
"app_version": "debian",
"capabilities": [
@@ -109,7 +109,7 @@
],
"title": "Steam Headless",
"train": "community",
- "version": "1.0.4"
+ "version": "1.0.5"
},
"schema": {
"groups": [
@@ -1442,6 +1442,18 @@
}
]
}
+ },
+ {
+ "variable": "gpus",
+ "group": "Resources Configuration",
+ "label": "GPU Configuration",
+ "schema": {
+ "type": "dict",
+ "$ref": [
+ "definitions/gpu_configuration"
+ ],
+ "attrs": []
+ }
}
]
}
diff --git a/trains/community/tailscale/app_versions.json b/trains/community/tailscale/app_versions.json
index 7b051193d4..420c5e05d1 100644
--- a/trains/community/tailscale/app_versions.json
+++ b/trains/community/tailscale/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/tailscale/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.78.3_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/tautulli/app_versions.json b/trains/community/tautulli/app_versions.json
index 77e716b682..e2a29a8c88 100644
--- a/trains/community/tautulli/app_versions.json
+++ b/trains/community/tautulli/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/tautulli/1.1.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v2.15.1_1.1.10",
"version": "1.1.10",
diff --git a/trains/community/tdarr/app_versions.json b/trains/community/tdarr/app_versions.json
index aad83b1e73..86809d39e1 100644
--- a/trains/community/tdarr/app_versions.json
+++ b/trains/community/tdarr/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/tdarr/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.29.01_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/terraria/app_versions.json b/trains/community/terraria/app_versions.json
index 899adbac63..234ead889f 100644
--- a/trains/community/terraria/app_versions.json
+++ b/trains/community/terraria/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/terraria/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "tshock-1.4.4.9-5.2.0-3_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/tftpd-hpa/app_versions.json b/trains/community/tftpd-hpa/app_versions.json
index 58f26a094c..ba809ea22e 100644
--- a/trains/community/tftpd-hpa/app_versions.json
+++ b/trains/community/tftpd-hpa/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/tftpd-hpa/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.0.0_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/tianji/app_versions.json b/trains/community/tianji/app_versions.json
index a5c220ed1c..053bd26d85 100644
--- a/trains/community/tianji/app_versions.json
+++ b/trains/community/tianji/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/tianji/1.0.5",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.17.9_1.0.5",
"version": "1.0.5",
diff --git a/trains/community/tiny-media-manager/1.1.7/README.md b/trains/community/tiny-media-manager/1.1.8/README.md
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/README.md
rename to trains/community/tiny-media-manager/1.1.8/README.md
diff --git a/trains/community/tiny-media-manager/1.1.7/app.yaml b/trains/community/tiny-media-manager/1.1.8/app.yaml
similarity index 97%
rename from trains/community/tiny-media-manager/1.1.7/app.yaml
rename to trains/community/tiny-media-manager/1.1.8/app.yaml
index 6381492e65..8048d1841a 100644
--- a/trains/community/tiny-media-manager/1.1.7/app.yaml
+++ b/trains/community/tiny-media-manager/1.1.8/app.yaml
@@ -1,4 +1,4 @@
-app_version: 5.0.13
+app_version: 5.1.1
capabilities:
- description: Tiny Media Manager is able to chown files.
name: CHOWN
@@ -39,4 +39,4 @@ sources:
- https://hub.docker.com/r/tinymediamanager/tinymediamanager
title: Tiny Media Manager
train: community
-version: 1.1.7
+version: 1.1.8
diff --git a/trains/community/tiny-media-manager/1.1.7/ix_values.yaml b/trains/community/tiny-media-manager/1.1.8/ix_values.yaml
similarity index 92%
rename from trains/community/tiny-media-manager/1.1.7/ix_values.yaml
rename to trains/community/tiny-media-manager/1.1.8/ix_values.yaml
index f753c58835..538ddf68a5 100644
--- a/trains/community/tiny-media-manager/1.1.7/ix_values.yaml
+++ b/trains/community/tiny-media-manager/1.1.8/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: tinymediamanager/tinymediamanager
- tag: 5.0.13
+ tag: 5.1.1
consts:
tiny_media_manager_container_name: tiny-media-manager
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migrate_from_kubernetes b/trains/community/tiny-media-manager/1.1.8/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migrate_from_kubernetes
rename to trains/community/tiny-media-manager/1.1.8/migrations/migrate_from_kubernetes
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/__init__.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/__init__.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/__init__.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/cpu.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/cpu.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/cpu.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/dns_config.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/dns_config.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/dns_config.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/kubernetes_secrets.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/memory.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/memory.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/memory.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/resources.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/resources.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/resources.py
diff --git a/trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/storage.py b/trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/migrations/migration_helpers/storage.py
rename to trains/community/tiny-media-manager/1.1.8/migrations/migration_helpers/storage.py
diff --git a/trains/community/tiny-media-manager/1.1.7/questions.yaml b/trains/community/tiny-media-manager/1.1.8/questions.yaml
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/questions.yaml
rename to trains/community/tiny-media-manager/1.1.8/questions.yaml
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/docker-compose.yaml b/trains/community/tiny-media-manager/1.1.8/templates/docker-compose.yaml
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/docker-compose.yaml
rename to trains/community/tiny-media-manager/1.1.8/templates/docker-compose.yaml
diff --git a/trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/configs.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/configs.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/configs.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/container.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/container.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/container.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/container.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/depends.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/depends.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/depends.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deploy.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deploy.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_perms.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_redis.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/device.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/device.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/device.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/device_cgroup_rules.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/device_cgroup_rules.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/devices.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/devices.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/devices.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/dns.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/dns.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/dns.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/environment.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/environment.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/environment.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/error.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/error.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/error.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/expose.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/expose.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/expose.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/extra_hosts.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/extra_hosts.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/extra_hosts.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/formatter.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/formatter.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/functions.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/functions.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/functions.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/healthcheck.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/labels.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/labels.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/labels.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/notes.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/notes.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/notes.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/portal.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/portal.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/portal.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/portals.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/portals.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/portals.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/ports.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/ports.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/ports.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/render.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/render.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/render.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/resources.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/resources.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/resources.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/restart.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/restart.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/restart.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/storage.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/storage.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/storage.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/sysctls.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/__init__.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/__init__.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_container.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_container.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_container.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_container.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_extra_hosts.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_extra_hosts.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/validations.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/validations.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/validations.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/validations.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_mount.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_sources.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_sources.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_sources.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_sources.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_types.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volumes.py b/trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/volumes.py
rename to trains/community/tiny-media-manager/1.1.8/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/tiny-media-manager/1.1.7/templates/test_values/basic-values.yaml b/trains/community/tiny-media-manager/1.1.8/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/community/tiny-media-manager/1.1.7/templates/test_values/basic-values.yaml
rename to trains/community/tiny-media-manager/1.1.8/templates/test_values/basic-values.yaml
diff --git a/trains/community/tiny-media-manager/app_versions.json b/trains/community/tiny-media-manager/app_versions.json
index 4e5a8f7a7f..1dca3b89b6 100644
--- a/trains/community/tiny-media-manager/app_versions.json
+++ b/trains/community/tiny-media-manager/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.1.7": {
+ "1.1.8": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/community/tiny-media-manager/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/community/tiny-media-manager/1.1.8",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "5.0.13_1.1.7",
- "version": "1.1.7",
+ "human_version": "5.1.1_1.1.8",
+ "version": "1.1.8",
"app_metadata": {
- "app_version": "5.0.13",
+ "app_version": "5.1.1",
"capabilities": [
{
"description": "Tiny Media Manager is able to chown files.",
@@ -67,7 +67,7 @@
],
"title": "Tiny Media Manager",
"train": "community",
- "version": "1.1.7"
+ "version": "1.1.8"
},
"schema": {
"groups": [
diff --git a/trains/community/transmission/app_versions.json b/trains/community/transmission/app_versions.json
index 5cbef366c0..9b56d16b43 100644
--- a/trains/community/transmission/app_versions.json
+++ b/trains/community/transmission/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/transmission/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "4.0.6_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/twofactor-auth/app_versions.json b/trains/community/twofactor-auth/app_versions.json
index a8a43a9bfb..7d38f34714 100644
--- a/trains/community/twofactor-auth/app_versions.json
+++ b/trains/community/twofactor-auth/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/twofactor-auth/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "5.4.3_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/umami/app_versions.json b/trains/community/umami/app_versions.json
index 409e68e7b8..d27967f9f3 100644
--- a/trains/community/umami/app_versions.json
+++ b/trains/community/umami/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/umami/1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v2.15.1_1.0.3",
"version": "1.0.3",
diff --git a/trains/community/unifi-controller/app_versions.json b/trains/community/unifi-controller/app_versions.json
index 5a9a60828e..4f3f0f4f4d 100644
--- a/trains/community/unifi-controller/app_versions.json
+++ b/trains/community/unifi-controller/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/unifi-controller/1.3.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "9.0.108_1.3.8",
"version": "1.3.8",
diff --git a/trains/community/unifi-protect-backup/app_versions.json b/trains/community/unifi-protect-backup/app_versions.json
index 4fc134d8cf..508301369b 100644
--- a/trains/community/unifi-protect-backup/app_versions.json
+++ b/trains/community/unifi-protect-backup/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/unifi-protect-backup/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.12.0_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/uptime-kuma/app_versions.json b/trains/community/uptime-kuma/app_versions.json
index 9f82187d5d..9afd396590 100644
--- a/trains/community/uptime-kuma/app_versions.json
+++ b/trains/community/uptime-kuma/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/uptime-kuma/1.0.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.0.0-beta.1_1.0.15",
"version": "1.0.15",
diff --git a/trains/community/urbackup/app_versions.json b/trains/community/urbackup/app_versions.json
index a488c8bd20..cbb0789a69 100644
--- a/trains/community/urbackup/app_versions.json
+++ b/trains/community/urbackup/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/urbackup/1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.5.x_1.0.2",
"version": "1.0.2",
diff --git a/trains/community/vaultwarden/app_versions.json b/trains/community/vaultwarden/app_versions.json
index 7f4c34a3ec..71674df560 100644
--- a/trains/community/vaultwarden/app_versions.json
+++ b/trains/community/vaultwarden/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/vaultwarden/1.2.10",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.33.0_1.2.10",
"version": "1.2.10",
diff --git a/trains/community/vikunja/app_versions.json b/trains/community/vikunja/app_versions.json
index 15190a3a67..223e424a22 100644
--- a/trains/community/vikunja/app_versions.json
+++ b/trains/community/vikunja/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/vikunja/1.4.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.24.6_1.4.8",
"version": "1.4.8",
diff --git a/trains/community/webdav/app_versions.json b/trains/community/webdav/app_versions.json
index 532988a054..22c3131eda 100644
--- a/trains/community/webdav/app_versions.json
+++ b/trains/community/webdav/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/webdav/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.4.63_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/whoogle/app_versions.json b/trains/community/whoogle/app_versions.json
index acd71f5d8a..4502e89d8d 100644
--- a/trains/community/whoogle/app_versions.json
+++ b/trains/community/whoogle/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/whoogle/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "0.9.3_1.1.9",
"version": "1.1.9",
diff --git a/trains/community/wordpress/app_versions.json b/trains/community/wordpress/app_versions.json
index 2d4b541621..d713ec9108 100644
--- a/trains/community/wordpress/app_versions.json
+++ b/trains/community/wordpress/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/wordpress/1.1.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "6.7.1_1.1.7",
"version": "1.1.7",
diff --git a/trains/community/wyze-bridge/1.0.0/README.md b/trains/community/wyze-bridge/1.0.0/README.md
new file mode 100644
index 0000000000..6f693910db
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/README.md
@@ -0,0 +1,3 @@
+# Wyze-Bridge
+
+[Wyze-Bridge](https://github.com/mrlt8/docker-wyze-bridge) Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras
diff --git a/trains/community/wyze-bridge/1.0.0/app.yaml b/trains/community/wyze-bridge/1.0.0/app.yaml
new file mode 100644
index 0000000000..b3462025c8
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/app.yaml
@@ -0,0 +1,43 @@
+app_version: 2.10.3
+capabilities:
+- description: Wyze Bridge is able to change file ownership.
+ name: CHOWN
+- description: Wyze Bridge is able to set the setuid attribute on a file.
+ name: SETUID
+- description: Wyze Bridge is able to set the setgid attribute on a file.
+ name: SETGID
+- description: Wyze Bridge is able to bypass permission checks on operations that
+ normally require the file system UID of the process to match the UID of the file.
+ name: FOWNER
+- description: Wyze Bridge is able to bypass file read, write, and execute permission
+ checks.
+ name: DAC_OVERRIDE
+categories:
+- security
+description: Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for
+ most of your Wyze cameras
+home: https://github.com/mrlt8/docker-wyze-bridge
+host_mounts: []
+icon: https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png
+keywords:
+- camera
+lib_version: 2.1.14
+lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
+maintainers:
+- email: dev@ixsystems.com
+ name: truenas
+ url: https://www.truenas.com/
+name: wyze-bridge
+run_as_context:
+- description: Wyze Bridge runs as the root user.
+ gid: 0
+ group_name: root
+ uid: 0
+ user_name: root
+screenshots:
+- https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png
+sources:
+- https://github.com/mrlt8/docker-wyze-bridge
+title: Wyze Bridge
+train: community
+version: 1.0.0
diff --git a/trains/community/wyze-bridge/1.0.0/ix_values.yaml b/trains/community/wyze-bridge/1.0.0/ix_values.yaml
new file mode 100644
index 0000000000..2e2de8faf7
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/ix_values.yaml
@@ -0,0 +1,17 @@
+images:
+ image:
+ repository: mrlt8/wyze-bridge
+ tag: 2.10.3
+
+consts:
+ wyze_container_name: wyze-bridge
+ config_container_name: config
+ data_path: /app/data
+ internal_rtmp_port: 1935
+ internal_rtsp_port: 8554
+ internal_hls_port: 8888
+ internal_webrtc_port: 8889
+ internal_webrtc_ice_port: 8189
+ notes_body: |
+ As of May 2024, you will need an API Key and API ID
+ from: https://support.wyze.com/hc/en-us/articles/16129834216731.
diff --git a/trains/community/wyze-bridge/1.0.0/questions.yaml b/trains/community/wyze-bridge/1.0.0/questions.yaml
new file mode 100644
index 0000000000..aa711331a3
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/questions.yaml
@@ -0,0 +1,588 @@
+groups:
+ - name: Wyze Bridge Configuration
+ description: Configure Wyze Bridge
+ - name: Network Configuration
+ description: Configure Network for Wyze Bridge
+ - name: Storage Configuration
+ description: Configure Storage for Wyze Bridge
+ - name: Labels Configuration
+ description: Configure Labels for Wyze Bridge
+ - name: Resources Configuration
+ description: Configure Resources for Wyze Bridge
+
+questions:
+ - variable: TZ
+ group: Wyze Bridge Configuration
+ label: Timezone
+ schema:
+ type: string
+ default: Etc/UTC
+ required: true
+ $ref:
+ - definitions/timezone
+ - variable: wyze
+ label: ""
+ group: Wyze Bridge Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: wb_auth
+ label: Wyze Bridge Authentication Enabled
+ description: Wyze Bridge Authentication Enabled
+ schema:
+ type: boolean
+ default: true
+ required: true
+ - variable: wb_username
+ label: Wyze Bridge Username
+ description: The Wyze Bridge UI username.
+ schema:
+ type: string
+ required: false
+ default: wbadmin
+ show_if: [["wb_auth", "=", true]]
+ - variable: wb_password
+ label: Wyze Bridge Password
+ description: The Wyze Bridge UI password.
+ schema:
+ type: string
+ required: false
+ default: wbadmin
+ show_if: [["wb_auth", "=", true]]
+ - variable: enable_audio
+ label: Enable Camera Audio
+ description: Optional - Enable Audio from Cameras
+ schema:
+ type: boolean
+ default: false
+ - variable: additional_envs
+ label: Additional Environment Variables
+ description: Configure additional environment variables for wyze bridge.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: env
+ label: Environment Variable
+ schema:
+ type: dict
+ attrs:
+ - variable: name
+ label: Name
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: network
+ label: ""
+ group: Network Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: web_port
+ label: WebUI Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "!=", ""]]
+ default: 35000
+ required: true
+ $ref:
+ - definitions/port
+ - variable: rtmp_port
+ label: RTMP Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30100
+ required: true
+ $ref:
+ - definitions/port
+ - variable: rtsp_port
+ label: RTSP Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30101
+ required: true
+ $ref:
+ - definitions/port
+ - variable: hls_port
+ label: HLS Port
+ schema:
+ type: dict
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30102
+ required: true
+ $ref:
+ - definitions/port
+ - variable: enable_webrtc
+ label: Enable WebRTC
+ description: Optional - Enable WebRTC for Wyze Bridge
+ schema:
+ type: boolean
+ default: false
+ - variable: webrtc_ip
+ label: WebRTC IP
+ description: Set this to the host IP to enable a WebRTC stream on port 8889
+ schema:
+ type: ipaddr
+ required: false
+ default: ""
+ show_if: [["enable_webrtc", "=", true]]
+ - variable: webrtc_port
+ label: WebRTC Port
+ schema:
+ type: dict
+ show_if: [["enable_webrtc", "=", true]]
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30103
+ required: true
+ $ref:
+ - definitions/port
+ - variable: webrtc_ice_port
+ label: WebRTC/ICE Port
+ schema:
+ type: dict
+ show_if: [["enable_webrtc", "=", true]]
+ attrs:
+ - variable: bind_mode
+ label: Port Bind Mode
+ description: |
+ The port bind mode.
+ - Publish: The port will be published on the host for external access.
+ - Expose: The port will be exposed for inter-container communication.
+ - None: The port will not be exposed or published.
+ Note: If the Dockerfile defines an EXPOSE directive,
+ the port will still be exposed for inter-container communication regardless of this setting.
+ schema:
+ type: string
+ default: "published"
+ enum:
+ - value: "published"
+ description: Publish port on the host for external access
+ - value: "exposed"
+ description: Expose port for inter-container communication
+ - value: ""
+ description: None
+ - variable: port_number
+ label: Port Number
+ schema:
+ type: int
+ show_if: [["bind_mode", "=", "published"]]
+ default: 30104
+ required: true
+ $ref:
+ - definitions/port
+ - variable: host_network
+ label: Host Network
+ description: |
+ Bind to the host network. It's recommended to keep this disabled.
+ schema:
+ type: boolean
+ default: false
+ - variable: storage
+ label: ""
+ group: Storage Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: data
+ label: wyze bridge Data Storage
+ description: The path to store wyze bridge Data.
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "ix_volume"
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ hidden: true
+ default: "data"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: additional_storage
+ label: Additional Storage
+ description: Additional storage for wyze bridge.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: storageEntry
+ label: Storage Entry
+ schema:
+ type: dict
+ attrs:
+ - variable: type
+ label: Type
+ description: |
+ ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system.
+ SMB Share: Is a SMB share that is mounted to as a volume.
+ schema:
+ type: string
+ required: true
+ default: "ix_volume"
+ immutable: true
+ enum:
+ - value: "host_path"
+ description: Host Path (Path that already exists on the system)
+ - value: "ix_volume"
+ description: ixVolume (Dataset created automatically by the system)
+ - value: "cifs"
+ description: SMB/CIFS Share (Mounts a volume to a SMB share)
+ - variable: read_only
+ label: Read Only
+ description: Mount the volume as read only.
+ schema:
+ type: boolean
+ default: false
+ - variable: mount_path
+ label: Mount Path
+ description: The path inside the container to mount the storage.
+ schema:
+ type: path
+ required: true
+ - variable: host_path_config
+ label: Host Path Configuration
+ schema:
+ type: dict
+ show_if: [["type", "=", "host_path"]]
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: acl
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: path
+ label: Host Path
+ description: The host path to use for storage.
+ schema:
+ type: hostpath
+ show_if: [["acl_enable", "=", false]]
+ required: true
+ - variable: ix_volume_config
+ label: ixVolume Configuration
+ description: The configuration for the ixVolume dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "ix_volume"]]
+ $ref:
+ - "normalize/ix_volume"
+ attrs:
+ - variable: acl_enable
+ label: Enable ACL
+ description: Enable ACL for the storage.
+ schema:
+ type: boolean
+ default: false
+ - variable: dataset_name
+ label: Dataset Name
+ description: The name of the dataset to use for storage.
+ schema:
+ type: string
+ required: true
+ immutable: true
+ default: "storage_entry"
+ - variable: acl_entries
+ label: ACL Configuration
+ schema:
+ type: dict
+ show_if: [["acl_enable", "=", true]]
+ attrs: []
+ $ref:
+ - "normalize/acl"
+ - variable: cifs_config
+ label: SMB Configuration
+ description: The configuration for the SMB dataset.
+ schema:
+ type: dict
+ show_if: [["type", "=", "cifs"]]
+ attrs:
+ - variable: server
+ label: Server
+ description: The server to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: path
+ label: Path
+ description: The path to mount the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: username
+ label: Username
+ description: The username to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ - variable: password
+ label: Password
+ description: The password to use for the SMB share.
+ schema:
+ type: string
+ required: true
+ private: true
+ - variable: domain
+ label: Domain
+ description: The domain to use for the SMB share.
+ schema:
+ type: string
+ - variable: labels
+ label: ""
+ group: Labels Configuration
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: label
+ label: Label
+ schema:
+ type: dict
+ attrs:
+ - variable: key
+ label: Key
+ schema:
+ type: string
+ required: true
+ - variable: value
+ label: Value
+ schema:
+ type: string
+ required: true
+ - variable: containers
+ label: Containers
+ description: Containers where the label should be applied
+ schema:
+ type: list
+ items:
+ - variable: container
+ label: Container
+ schema:
+ type: string
+ required: true
+ enum:
+ - value: wyze-bridge
+ description: wyze-bridge
+ - variable: resources
+ label: ""
+ group: Resources Configuration
+ schema:
+ type: dict
+ attrs:
+ - variable: limits
+ label: Limits
+ schema:
+ type: dict
+ attrs:
+ - variable: cpus
+ label: CPUs
+ description: CPUs limit for wyze bridge.
+ schema:
+ type: int
+ default: 2
+ required: true
+ - variable: memory
+ label: Memory (in MB)
+ description: Memory limit for wyze bridge.
+ schema:
+ type: int
+ default: 4096
+ required: true
diff --git a/trains/community/wyze-bridge/1.0.0/templates/docker-compose.yaml b/trains/community/wyze-bridge/1.0.0/templates/docker-compose.yaml
new file mode 100644
index 0000000000..08364aef60
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/docker-compose.yaml
@@ -0,0 +1,39 @@
+{% set tpl = ix_lib.base.render.Render(values) %}
+
+{% set c1 = tpl.add_container(values.consts.wyze_container_name, "image") %}
+{% do c1.add_caps(["CHOWN", "DAC_OVERRIDE", "FOWNER", "KILL", "SETGID", "SETUID"])%}
+
+{% do c1.set_command(["flask", "run", "--host", "0.0.0.0", "--port", values.network.web_port.port_number]) %}
+
+{% do c1.healthcheck.set_test("tcp", {"port": values.network.web_port.port_number}) %}
+
+{% do c1.environment.add_env("ENABLE_AUDIO", values.wyze.enable_audio) %}
+{% do c1.environment.add_env("WB_AUTH", values.wyze.wb_auth) %}
+{% if values.wyze.wb_auth %}
+ {% do c1.environment.add_env("WB_USERNAME", values.wyze.wb_username) %}
+ {% do c1.environment.add_env("WB_PASSWORD", values.wyze.wb_password) %}
+{% endif %}
+
+{% if values.network.enable_webrtc %}
+ {% do c1.environment.add_env("WB_IP", values.network.webrtc_ip) %}
+ {% do c1.add_port(values.network.webrtc_port, {"container_port": values.consts.internal_webrtc_port}) %}
+ {% do c1.add_port(values.network.webrtc_ice_port, {"container_port": values.consts.internal_webrtc_ice_port, "protocol": "udp"}) %}
+{% endif %}
+
+{% do c1.environment.add_user_envs(values.wyze.additional_envs) %}
+
+{% do c1.add_port(values.network.web_port) %}
+{% do c1.add_port(values.network.rtmp_port, {"container_port": values.consts.internal_rtmp_port}) %}
+{% do c1.add_port(values.network.rtsp_port, {"container_port": values.consts.internal_rtsp_port}) %}
+{% do c1.add_port(values.network.hls_port, {"container_port": values.consts.internal_hls_port}) %}
+
+{% do c1.add_storage(values.consts.data_path, values.storage.data) %}
+
+{% for store in values.storage.additional_storage %}
+ {% do c1.add_storage(store.mount_path, store) %}
+{% endfor %}
+
+{% do tpl.portals.add_portal({"port": values.network.web_port.port_number}) %}
+{% do tpl.notes.set_body(values.consts.notes_body) %}
+
+{{ tpl.render() | tojson }}
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/__init__.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/__init__.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/configs.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/configs.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/configs.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/configs.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/container.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/container.py
similarity index 92%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/container.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/container.py
index d0abd6edc7..8e889be045 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/container.py
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/container.py
@@ -8,21 +8,24 @@
from .configs import ContainerConfigs
from .depends import Depends
from .deploy import Deploy
+ from .device_cgroup_rules import DeviceCGroupRules
from .devices import Devices
from .dns import Dns
from .environment import Environment
from .error import RenderError
from .expose import Expose
+ from .extra_hosts import ExtraHosts
from .formatter import escape_dollar, get_image_with_hashed_data
from .healthcheck import Healthcheck
from .labels import Labels
from .ports import Ports
from .restart import RestartPolicy
from .validations import (
- valid_network_mode_or_raise,
valid_cap_or_raise,
- valid_pull_policy_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
)
from .storage import Storage
from .sysctls import Sysctls
@@ -30,21 +33,24 @@
from configs import ContainerConfigs
from depends import Depends
from deploy import Deploy
+ from device_cgroup_rules import DeviceCGroupRules
from devices import Devices
from dns import Dns
from environment import Environment
from error import RenderError
from expose import Expose
+ from extra_hosts import ExtraHosts
from formatter import escape_dollar, get_image_with_hashed_data
from healthcheck import Healthcheck
from labels import Labels
from ports import Ports
from restart import RestartPolicy
from validations import (
- valid_network_mode_or_raise,
valid_cap_or_raise,
- valid_pull_policy_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
)
from storage import Storage
from sysctls import Sysctls
@@ -63,6 +69,7 @@ def __init__(self, render_instance: "Render", name: str, image: str):
self._stdin_open: bool = False
self._init: bool | None = None
self._read_only: bool | None = None
+ self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
self._hostname: str = ""
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
self._cap_add: set[str] = set()
@@ -75,6 +82,8 @@ def __init__(self, render_instance: "Render", name: str, image: str):
self._grace_period: int | None = None
self._shm_size: int | None = None
self._storage: Storage = Storage(self._render_instance)
+ self._ipc_mode: str | None = None
+ self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
self.sysctls: Sysctls = Sysctls(self._render_instance, self)
self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
self.deploy: Deploy = Deploy(self._render_instance)
@@ -154,6 +163,9 @@ def set_user(self, user: int, group: int):
raise RenderError(f"User/Group [{i}] is not valid")
self._user = f"{user}:{group}"
+ def add_extra_host(self, host: str, ip: str):
+ self._extra_hosts.add_host(host, ip)
+
def add_group(self, group: int | str):
if isinstance(group, str):
group = str(group).strip()
@@ -182,6 +194,12 @@ def set_tty(self, enabled: bool = False):
def set_stdin(self, enabled: bool = False):
self._stdin_open = enabled
+ def set_ipc_mode(self, ipc_mode: str):
+ self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+ def add_device_cgroup_rule(self, dev_grp_rule: str):
+ self._device_cgroup_rules.add_rule(dev_grp_rule)
+
def set_init(self, enabled: bool = False):
self._init = enabled
@@ -235,7 +253,7 @@ def add_port(self, port_config: dict | None = None, dev_config: dict | None = No
host_port = config.get("port_number", 0)
container_port = config.get("container_port", 0) or host_port
protocol = config.get("protocol", "tcp")
- host_ips = config.get("host_ips", ["0.0.0.0", "::"])
+ host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
if not isinstance(host_ips, list):
raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
@@ -309,6 +327,15 @@ def render(self) -> dict[str, Any]:
if self.configs.has_configs():
result["configs"] = self.configs.render()
+ if self._ipc_mode is not None:
+ result["ipc"] = self._ipc_mode
+
+ if self._device_cgroup_rules.has_rules():
+ result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+ if self._extra_hosts.has_hosts():
+ result["extra_hosts"] = self._extra_hosts.render()
+
if self._init is not None:
result["init"] = self._init
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/depends.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/depends.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/depends.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/depends.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deploy.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deploy.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deploy.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deploy.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_mariadb.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_mariadb.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_mariadb.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_mariadb.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_perms.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_perms.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_perms.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_perms.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_postgres.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_postgres.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_postgres.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_postgres.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_redis.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_redis.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/deps_redis.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/deps_redis.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/device.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/device.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/device.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/device.py
diff --git a/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/device_cgroup_rules.py
new file mode 100644
index 0000000000..dcccfee773
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/device_cgroup_rules.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+ def __init__(self, rule: str):
+ rule = valid_device_cgroup_rule_or_raise(rule)
+ parts = rule.split(" ")
+ major, minor = parts[1].split(":")
+
+ self._type = parts[0]
+ self._major = major
+ self._minor = minor
+ self._permissions = parts[2]
+
+ def get_key(self):
+ return f"{self._type}_{self._major}_{self._minor}"
+
+ def render(self):
+ return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._rules: set[DeviceCGroupRule] = set()
+ self._track_rule_combos: set[str] = set()
+
+ def add_rule(self, rule: str):
+ dev_group_rule = DeviceCGroupRule(rule)
+ if dev_group_rule in self._rules:
+ raise RenderError(f"Device Group Rule [{rule}] already added")
+
+ rule_key = dev_group_rule.get_key()
+ if rule_key in self._track_rule_combos:
+ raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+ self._rules.add(dev_group_rule)
+ self._track_rule_combos.add(rule_key)
+
+ def has_rules(self):
+ return len(self._rules) > 0
+
+ def render(self):
+ return sorted([rule.render() for rule in self._rules])
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/devices.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/devices.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/devices.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/devices.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/dns.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/dns.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/dns.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/dns.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/environment.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/environment.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/environment.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/environment.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/error.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/error.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/error.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/error.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/expose.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/expose.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/expose.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/expose.py
diff --git a/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/extra_hosts.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/extra_hosts.py
new file mode 100644
index 0000000000..eaad3bed26
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/extra_hosts.py
@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+
+class ExtraHosts:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._extra_hosts: dict[str, str] = {}
+
+ def add_host(self, host: str, ip: str):
+ if not ip == "host-gateway":
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+ if host in self._extra_hosts:
+ raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+ self._extra_hosts[host] = ip
+
+ def has_hosts(self):
+ return len(self._extra_hosts) > 0
+
+ def render(self):
+ return {host: ip for host, ip in self._extra_hosts.items()}
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/formatter.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/formatter.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/formatter.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/formatter.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/functions.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/functions.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/functions.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/functions.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/healthcheck.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/healthcheck.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/healthcheck.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/healthcheck.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/labels.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/labels.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/labels.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/labels.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/notes.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/notes.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/notes.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/notes.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/portal.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/portal.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/portal.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/portal.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/portals.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/portals.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/portals.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/portals.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/ports.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/ports.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/ports.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/ports.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/render.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/render.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/render.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/render.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/resources.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/resources.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/resources.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/resources.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/restart.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/restart.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/restart.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/restart.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/storage.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/storage.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/storage.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/storage.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/sysctls.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/sysctls.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/sysctls.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/sysctls.py
diff --git a/trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/__init__.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_build_image.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_build_image.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_build_image.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_configs.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_configs.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_configs.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_configs.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_container.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_container.py
similarity index 85%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_container.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_container.py
index bb3d98dffc..1980dcd5df 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/tests/test_container.py
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_container.py
@@ -367,3 +367,59 @@ def test_add_ports_with_invalid_host_ips(mock_values):
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+ ]
+
+
+def test_set_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c2 = render.add_container("test_container2", "test_image")
+ c2.healthcheck.disable()
+ c1.set_ipc_mode("container:test_container2")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("container:invalid")
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_depends.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_depends.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_depends.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_depends.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_deps.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_deps.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_deps.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_deps.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_device.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_device.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_device.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_device.py
diff --git a/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
new file mode 100644
index 0000000000..581fe82017
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_device_cgroup_rule(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ c1.add_device_cgroup_rule("b 10:20 rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["device_cgroup_rules"] == [
+ "b 10:20 rwm",
+ "c 13:* rwm",
+ ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 rwd")
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_dns.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_dns.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_dns.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_dns.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_environment.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_environment.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_environment.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_environment.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_expose.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_expose.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_expose.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_expose.py
diff --git a/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
new file mode 100644
index 0000000000..35230be16e
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ c1.add_extra_host("test_host2", "127.0.0.2")
+ c1.add_extra_host("host.docker.internal", "host-gateway")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {
+ "host.docker.internal": "host-gateway",
+ "test_host": "127.0.0.1",
+ "test_host2": "127.0.0.2",
+ }
+
+
+def test_add_duplicate_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "::1")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "invalid_ip")
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_formatter.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_formatter.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_formatter.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_functions.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_functions.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_functions.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_functions.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_healthcheck.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_labels.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_labels.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_labels.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_labels.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_notes.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_notes.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_notes.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_notes.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_portal.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_portal.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_portal.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_portal.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_ports.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_ports.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_ports.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_ports.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_render.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_render.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_render.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_render.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_resources.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_resources.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_resources.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_resources.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_restart.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_restart.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_restart.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_restart.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_sysctls.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_sysctls.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_sysctls.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_validations.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_validations.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_validations.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_validations.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_volumes.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/test_volumes.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/tests/test_volumes.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/validations.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/validations.py
similarity index 85%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/validations.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/validations.py
index 3b0722afbc..d4aa633006 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/validations.py
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/validations.py
@@ -38,6 +38,17 @@ def valid_pull_policy_or_raise(pull_policy: str):
return pull_policy
+def valid_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+ valid_modes = ("", "host", "private", "shareable", "none")
+ if ipc_mode in valid_modes:
+ return ipc_mode
+ if ipc_mode.startswith("container:"):
+ if ipc_mode[10:] not in containers:
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+ return ipc_mode
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
def valid_sysctl_or_raise(sysctl: str, host_network: bool):
if not sysctl:
raise RenderError("Sysctl cannot be empty")
@@ -136,6 +147,33 @@ def valid_cgroup_perm_or_raise(cgroup_perm: str):
return cgroup_perm
+def valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+ parts = dev_grp_rule.split(" ")
+ if len(parts) != 3:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [ : ]"
+ )
+
+ valid_types = ("a", "b", "c")
+ if parts[0] not in valid_types:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+ f" but got [{parts[0]}]"
+ )
+
+ major, minor = parts[1].split(":")
+ for part in (major, minor):
+ if part != "*" and not part.isdigit():
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+ f" or [*] but got [{major}] and [{minor}]"
+ )
+
+ valid_cgroup_perm_or_raise(parts[2])
+
+ return dev_grp_rule
+
+
def allowed_dns_opt_or_raise(dns_opt: str):
disallowed_dns_opts = []
if dns_opt in disallowed_dns_opts:
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_mount.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_mount.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_mount.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_mount.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_mount_types.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_mount_types.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_mount_types.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_mount_types.py
diff --git a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_sources.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_sources.py
similarity index 93%
rename from trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_sources.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_sources.py
index dcfce44b75..6aba23df23 100644
--- a/trains/community/iconik-storage-gateway/1.0.9/templates/library/base_v2_1_9/volume_sources.py
+++ b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_sources.py
@@ -32,7 +32,9 @@ def __init__(self, render_instance: "Render", config: "IxStorageHostPathConfig")
path = valid_fs_path_or_raise(config.get("path", ""))
path = path.rstrip("/")
- self.source = allowed_fs_host_path_or_raise(path)
+ # TODO: Hack for Nextcloud deprecated config. Remove once we remove support for it
+ allow_unsafe_ix_volume = config.get("allow_unsafe_ix_volume", False)
+ self.source = allowed_fs_host_path_or_raise(path, allow_unsafe_ix_volume)
def get(self):
return self.source
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_types.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_types.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volume_types.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volume_types.py
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volumes.py b/trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volumes.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/volumes.py
rename to trains/community/wyze-bridge/1.0.0/templates/library/base_v2_1_14/volumes.py
diff --git a/trains/community/wyze-bridge/1.0.0/templates/test_values/basic-values.yaml b/trains/community/wyze-bridge/1.0.0/templates/test_values/basic-values.yaml
new file mode 100644
index 0000000000..eabff442c2
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/test_values/basic-values.yaml
@@ -0,0 +1,38 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: true
+ wb_username: wb-admin
+ wb_password: wb-password
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/trains/community/wyze-bridge/1.0.0/templates/test_values/noauth-values.yaml b/trains/community/wyze-bridge/1.0.0/templates/test_values/noauth-values.yaml
new file mode 100644
index 0000000000..a2c5d272f9
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/test_values/noauth-values.yaml
@@ -0,0 +1,37 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: false
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ host_ip: 0.0.0.0
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/trains/community/wyze-bridge/1.0.0/templates/test_values/webrtc-values.yaml b/trains/community/wyze-bridge/1.0.0/templates/test_values/webrtc-values.yaml
new file mode 100644
index 0000000000..495ee172f3
--- /dev/null
+++ b/trains/community/wyze-bridge/1.0.0/templates/test_values/webrtc-values.yaml
@@ -0,0 +1,46 @@
+resources:
+ limits:
+ cpus: 2.0
+ memory: 4096
+
+wyze:
+ wb_auth: true
+ wb_username: wb-admin
+ wb_password: wb-password
+ enable_audio: true
+ additional_envs: []
+
+network:
+ host_network: false
+ additional_ports: []
+ web_port:
+ bind_mode: published
+ port_number: 35000
+ rtmp_port:
+ bind_mode: published
+ port_number: 1935
+ rtsp_port:
+ bind_mode: published
+ port_number: 8554
+ hls_port:
+ bind_mode: published
+ port_number: 8888
+ enable_webrtc: true
+ webrtc_ip: 127.0.0.1
+ webrtc_port:
+ bind_mode: published
+ port_number: 8889
+ webrtc_ice_port:
+ bind_mode: published
+ port_number: 8189
+
+ix_volumes:
+ data: /opt/tests/mnt/data
+
+storage:
+ data:
+ type: ix_volume
+ ix_volume_config:
+ dataset_name: data
+ create_host_path: true
+ additional_storage: []
diff --git a/trains/community/wyze-bridge/app_versions.json b/trains/community/wyze-bridge/app_versions.json
new file mode 100644
index 0000000000..359089aecf
--- /dev/null
+++ b/trains/community/wyze-bridge/app_versions.json
@@ -0,0 +1,1077 @@
+{
+ "1.0.0": {
+ "healthy": true,
+ "supported": true,
+ "healthy_error": null,
+ "location": "/__w/apps/apps/trains/community/wyze-bridge/1.0.0",
+ "last_update": "2025-01-31 17:34:15",
+ "required_features": [],
+ "human_version": "2.10.3_1.0.0",
+ "version": "1.0.0",
+ "app_metadata": {
+ "app_version": "2.10.3",
+ "capabilities": [
+ {
+ "description": "Wyze Bridge is able to change file ownership.",
+ "name": "CHOWN"
+ },
+ {
+ "description": "Wyze Bridge is able to set the setuid attribute on a file.",
+ "name": "SETUID"
+ },
+ {
+ "description": "Wyze Bridge is able to set the setgid attribute on a file.",
+ "name": "SETGID"
+ },
+ {
+ "description": "Wyze Bridge is able to bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.",
+ "name": "FOWNER"
+ },
+ {
+ "description": "Wyze Bridge is able to bypass file read, write, and execute permission checks.",
+ "name": "DAC_OVERRIDE"
+ }
+ ],
+ "categories": [
+ "security"
+ ],
+ "description": "Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras",
+ "home": "https://github.com/mrlt8/docker-wyze-bridge",
+ "host_mounts": [],
+ "icon": "https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png",
+ "keywords": [
+ "camera"
+ ],
+ "lib_version": "2.1.14",
+ "lib_version_hash": "982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc",
+ "maintainers": [
+ {
+ "email": "dev@ixsystems.com",
+ "name": "truenas",
+ "url": "https://www.truenas.com/"
+ }
+ ],
+ "name": "wyze-bridge",
+ "run_as_context": [
+ {
+ "description": "Wyze Bridge runs as the root user.",
+ "gid": 0,
+ "group_name": "root",
+ "uid": 0,
+ "user_name": "root"
+ }
+ ],
+ "screenshots": [
+ "https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png"
+ ],
+ "sources": [
+ "https://github.com/mrlt8/docker-wyze-bridge"
+ ],
+ "title": "Wyze Bridge",
+ "train": "community",
+ "version": "1.0.0"
+ },
+ "schema": {
+ "groups": [
+ {
+ "name": "Wyze Bridge Configuration",
+ "description": "Configure Wyze Bridge"
+ },
+ {
+ "name": "Network Configuration",
+ "description": "Configure Network for Wyze Bridge"
+ },
+ {
+ "name": "Storage Configuration",
+ "description": "Configure Storage for Wyze Bridge"
+ },
+ {
+ "name": "Labels Configuration",
+ "description": "Configure Labels for Wyze Bridge"
+ },
+ {
+ "name": "Resources Configuration",
+ "description": "Configure Resources for Wyze Bridge"
+ }
+ ],
+ "questions": [
+ {
+ "variable": "TZ",
+ "group": "Wyze Bridge Configuration",
+ "label": "Timezone",
+ "schema": {
+ "type": "string",
+ "default": "Etc/UTC",
+ "required": true,
+ "$ref": [
+ "definitions/timezone"
+ ]
+ }
+ },
+ {
+ "variable": "wyze",
+ "label": "",
+ "group": "Wyze Bridge Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "wb_auth",
+ "label": "Wyze Bridge Authentication Enabled",
+ "description": "Wyze Bridge Authentication Enabled",
+ "schema": {
+ "type": "boolean",
+ "default": true,
+ "required": true
+ }
+ },
+ {
+ "variable": "wb_username",
+ "label": "Wyze Bridge Username",
+ "description": "The Wyze Bridge UI username.",
+ "schema": {
+ "type": "string",
+ "required": false,
+ "default": "wbadmin",
+ "show_if": [
+ [
+ "wb_auth",
+ "=",
+ true
+ ]
+ ]
+ }
+ },
+ {
+ "variable": "wb_password",
+ "label": "Wyze Bridge Password",
+ "description": "The Wyze Bridge UI password.",
+ "schema": {
+ "type": "string",
+ "required": false,
+ "default": "wbadmin",
+ "show_if": [
+ [
+ "wb_auth",
+ "=",
+ true
+ ]
+ ]
+ }
+ },
+ {
+ "variable": "enable_audio",
+ "label": "Enable Camera Audio",
+ "description": "Optional - Enable Audio from Cameras",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "additional_envs",
+ "label": "Additional Environment Variables",
+ "description": "Configure additional environment variables for wyze bridge.",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "env",
+ "label": "Environment Variable",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "name",
+ "label": "Name",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "value",
+ "label": "Value",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "network",
+ "label": "",
+ "group": "Network Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "web_port",
+ "label": "WebUI Port",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "!=",
+ ""
+ ]
+ ],
+ "default": 35000,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "rtmp_port",
+ "label": "RTMP Port",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "=",
+ "published"
+ ]
+ ],
+ "default": 30100,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "rtsp_port",
+ "label": "RTSP Port",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "=",
+ "published"
+ ]
+ ],
+ "default": 30101,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "hls_port",
+ "label": "HLS Port",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "=",
+ "published"
+ ]
+ ],
+ "default": 30102,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "enable_webrtc",
+ "label": "Enable WebRTC",
+ "description": "Optional - Enable WebRTC for Wyze Bridge",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "webrtc_ip",
+ "label": "WebRTC IP",
+ "description": "Set this to the host IP to enable a WebRTC stream on port 8889",
+ "schema": {
+ "type": "ipaddr",
+ "required": false,
+ "default": "",
+ "show_if": [
+ [
+ "enable_webrtc",
+ "=",
+ true
+ ]
+ ]
+ }
+ },
+ {
+ "variable": "webrtc_port",
+ "label": "WebRTC Port",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "enable_webrtc",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "=",
+ "published"
+ ]
+ ],
+ "default": 30103,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "webrtc_ice_port",
+ "label": "WebRTC/ICE Port",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "enable_webrtc",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "bind_mode",
+ "label": "Port Bind Mode",
+ "description": "The port bind mode.\n- Publish: The port will be published on the host for external access.\n- Expose: The port will be exposed for inter-container communication.\n- None: The port will not be exposed or published.\nNote: If the Dockerfile defines an EXPOSE directive,\nthe port will still be exposed for inter-container communication regardless of this setting.\n",
+ "schema": {
+ "type": "string",
+ "default": "published",
+ "enum": [
+ {
+ "value": "published",
+ "description": "Publish port on the host for external access"
+ },
+ {
+ "value": "exposed",
+ "description": "Expose port for inter-container communication"
+ },
+ {
+ "value": "",
+ "description": "None"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "port_number",
+ "label": "Port Number",
+ "schema": {
+ "type": "int",
+ "show_if": [
+ [
+ "bind_mode",
+ "=",
+ "published"
+ ]
+ ],
+ "default": 30104,
+ "required": true,
+ "$ref": [
+ "definitions/port"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "host_network",
+ "label": "Host Network",
+ "description": "Bind to the host network. It's recommended to keep this disabled.\n",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "storage",
+ "label": "",
+ "group": "Storage Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "data",
+ "label": "wyze bridge Data Storage",
+ "description": "The path to store wyze bridge Data.",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "type",
+ "label": "Type",
+ "description": "ixVolume: Is dataset created automatically by the system.\nHost Path: Is a path that already exists on the system.\n",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "immutable": true,
+ "default": "ix_volume",
+ "enum": [
+ {
+ "value": "host_path",
+ "description": "Host Path (Path that already exists on the system)"
+ },
+ {
+ "value": "ix_volume",
+ "description": "ixVolume (Dataset created automatically by the system)"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "ix_volume_config",
+ "label": "ixVolume Configuration",
+ "description": "The configuration for the ixVolume dataset.",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "ix_volume"
+ ]
+ ],
+ "$ref": [
+ "normalize/ix_volume"
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "dataset_name",
+ "label": "Dataset Name",
+ "description": "The name of the dataset to use for storage.",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "immutable": true,
+ "hidden": true,
+ "default": "data"
+ }
+ },
+ {
+ "variable": "acl_entries",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": []
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "host_path_config",
+ "label": "Host Path Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "host_path"
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "acl",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [],
+ "$ref": [
+ "normalize/acl"
+ ]
+ }
+ },
+ {
+ "variable": "path",
+ "label": "Host Path",
+ "description": "The host path to use for storage.",
+ "schema": {
+ "type": "hostpath",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ false
+ ]
+ ],
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "additional_storage",
+ "label": "Additional Storage",
+ "description": "Additional storage for wyze bridge.",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "storageEntry",
+ "label": "Storage Entry",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "type",
+ "label": "Type",
+ "description": "ixVolume: Is dataset created automatically by the system.\nHost Path: Is a path that already exists on the system.\nSMB Share: Is a SMB share that is mounted to as a volume.\n",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "default": "ix_volume",
+ "immutable": true,
+ "enum": [
+ {
+ "value": "host_path",
+ "description": "Host Path (Path that already exists on the system)"
+ },
+ {
+ "value": "ix_volume",
+ "description": "ixVolume (Dataset created automatically by the system)"
+ },
+ {
+ "value": "cifs",
+ "description": "SMB/CIFS Share (Mounts a volume to a SMB share)"
+ }
+ ]
+ }
+ },
+ {
+ "variable": "read_only",
+ "label": "Read Only",
+ "description": "Mount the volume as read only.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "mount_path",
+ "label": "Mount Path",
+ "description": "The path inside the container to mount the storage.",
+ "schema": {
+ "type": "path",
+ "required": true
+ }
+ },
+ {
+ "variable": "host_path_config",
+ "label": "Host Path Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "host_path"
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "acl",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [],
+ "$ref": [
+ "normalize/acl"
+ ]
+ }
+ },
+ {
+ "variable": "path",
+ "label": "Host Path",
+ "description": "The host path to use for storage.",
+ "schema": {
+ "type": "hostpath",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ false
+ ]
+ ],
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "ix_volume_config",
+ "label": "ixVolume Configuration",
+ "description": "The configuration for the ixVolume dataset.",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "ix_volume"
+ ]
+ ],
+ "$ref": [
+ "normalize/ix_volume"
+ ],
+ "attrs": [
+ {
+ "variable": "acl_enable",
+ "label": "Enable ACL",
+ "description": "Enable ACL for the storage.",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "variable": "dataset_name",
+ "label": "Dataset Name",
+ "description": "The name of the dataset to use for storage.",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "immutable": true,
+ "default": "storage_entry"
+ }
+ },
+ {
+ "variable": "acl_entries",
+ "label": "ACL Configuration",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "acl_enable",
+ "=",
+ true
+ ]
+ ],
+ "attrs": [],
+ "$ref": [
+ "normalize/acl"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "cifs_config",
+ "label": "SMB Configuration",
+ "description": "The configuration for the SMB dataset.",
+ "schema": {
+ "type": "dict",
+ "show_if": [
+ [
+ "type",
+ "=",
+ "cifs"
+ ]
+ ],
+ "attrs": [
+ {
+ "variable": "server",
+ "label": "Server",
+ "description": "The server to mount the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "path",
+ "label": "Path",
+ "description": "The path to mount the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "username",
+ "label": "Username",
+ "description": "The username to use for the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "password",
+ "label": "Password",
+ "description": "The password to use for the SMB share.",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "private": true
+ }
+ },
+ {
+ "variable": "domain",
+ "label": "Domain",
+ "description": "The domain to use for the SMB share.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "labels",
+ "label": "",
+ "group": "Labels Configuration",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "label",
+ "label": "Label",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "key",
+ "label": "Key",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "value",
+ "label": "Value",
+ "schema": {
+ "type": "string",
+ "required": true
+ }
+ },
+ {
+ "variable": "containers",
+ "label": "Containers",
+ "description": "Containers where the label should be applied",
+ "schema": {
+ "type": "list",
+ "items": [
+ {
+ "variable": "container",
+ "label": "Container",
+ "schema": {
+ "type": "string",
+ "required": true,
+ "enum": [
+ {
+ "value": "wyze-bridge",
+ "description": "wyze-bridge"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "variable": "resources",
+ "label": "",
+ "group": "Resources Configuration",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "limits",
+ "label": "Limits",
+ "schema": {
+ "type": "dict",
+ "attrs": [
+ {
+ "variable": "cpus",
+ "label": "CPUs",
+ "description": "CPUs limit for wyze bridge.",
+ "schema": {
+ "type": "int",
+ "default": 2,
+ "required": true
+ }
+ },
+ {
+ "variable": "memory",
+ "label": "Memory (in MB)",
+ "description": "Memory limit for wyze bridge.",
+ "schema": {
+ "type": "int",
+ "default": 4096,
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "readme": "Wyze-Bridge
Wyze-Bridge Create a local WebRTC, RTSP, RTMP, or HLS/Low-Latency HLS stream for most of your Wyze cameras
",
+ "changelog": null
+ }
+}
\ No newline at end of file
diff --git a/trains/community/wyze-bridge/item.yaml b/trains/community/wyze-bridge/item.yaml
new file mode 100644
index 0000000000..69b78f0e9e
--- /dev/null
+++ b/trains/community/wyze-bridge/item.yaml
@@ -0,0 +1,7 @@
+categories:
+- security
+icon_url: https://media.sys.truenas.net/apps/wyze-bridge/icons/icon.png
+screenshots:
+- https://media.sys.truenas.net/apps/wyze-bridge/screenshots/screenshot1.png
+tags:
+- camera
diff --git a/trains/community/zerotier/app_versions.json b/trains/community/zerotier/app_versions.json
index 1d31005779..c96b833a7a 100644
--- a/trains/community/zerotier/app_versions.json
+++ b/trains/community/zerotier/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/zerotier/1.1.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.14.2_1.1.8",
"version": "1.1.8",
diff --git a/trains/community/zigbee2mqtt/app_versions.json b/trains/community/zigbee2mqtt/app_versions.json
index 3140efdcee..9a194f277a 100644
--- a/trains/community/zigbee2mqtt/app_versions.json
+++ b/trains/community/zigbee2mqtt/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/community/zigbee2mqtt/1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.0.0_1.0.6",
"version": "1.0.6",
diff --git a/trains/dev/truenas-webui/app_versions.json b/trains/dev/truenas-webui/app_versions.json
index 6b0d854f41..c3056acfc9 100644
--- a/trains/dev/truenas-webui/app_versions.json
+++ b/trains/dev/truenas-webui/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/dev/truenas-webui/1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "latest_1.0.3",
"version": "1.0.3",
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/README.md b/trains/enterprise/asigra-ds-system/1.0.27/README.md
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/README.md
rename to trains/enterprise/asigra-ds-system/1.0.27/README.md
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/app.yaml b/trains/enterprise/asigra-ds-system/1.0.27/app.yaml
similarity index 98%
rename from trains/enterprise/asigra-ds-system/1.0.26/app.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/app.yaml
index 0d9db1c075..1c8489c68e 100644
--- a/trains/enterprise/asigra-ds-system/1.0.26/app.yaml
+++ b/trains/enterprise/asigra-ds-system/1.0.27/app.yaml
@@ -47,4 +47,4 @@ sources:
- https://hub.docker.com/r/asigra/ds-system
title: Asigra DS-System
train: enterprise
-version: 1.0.26
+version: 1.0.27
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/ix_values.yaml b/trains/enterprise/asigra-ds-system/1.0.27/ix_values.yaml
similarity index 97%
rename from trains/enterprise/asigra-ds-system/1.0.26/ix_values.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/ix_values.yaml
index 1256c77a81..cff21d9cce 100644
--- a/trains/enterprise/asigra-ds-system/1.0.26/ix_values.yaml
+++ b/trains/enterprise/asigra-ds-system/1.0.27/ix_values.yaml
@@ -7,7 +7,7 @@ images:
tag: 16.6
haproxy_image:
repository: haproxy
- tag: 3.1.2
+ tag: 3.1.3
consts:
asigra_container_name: dssystem
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/questions.yaml b/trains/enterprise/asigra-ds-system/1.0.27/questions.yaml
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/questions.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/questions.yaml
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/docker-compose.yaml b/trains/enterprise/asigra-ds-system/1.0.27/templates/docker-compose.yaml
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/docker-compose.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/docker-compose.yaml
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/__init__.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/__init__.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/__init__.py
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/__init__.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/configs.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/configs.py
new file mode 100644
index 0000000000..b76f4b169c
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/configs.py
@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._configs: dict[str, dict] = {}
+
+ def add(self, name: str, data: str):
+ if not isinstance(data, str):
+ raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+ if name not in self._configs:
+ self._configs[name] = {"name": name, "data": data}
+ return
+
+ if data == self._configs[name]["data"]:
+ return
+
+ raise RenderError(f"Config [{name}] already added with different data")
+
+ def has_configs(self):
+ return bool(self._configs)
+
+ def render(self):
+ return {
+ c["name"]: {"content": escape_dollar(c["data"])}
+ for c in sorted(self._configs.values(), key=lambda c: c["name"])
+ }
+
+
+class ContainerConfigs:
+ def __init__(self, render_instance: "Render", configs: Configs):
+ self._render_instance = render_instance
+ self.top_level_configs: Configs = configs
+ self.container_configs: set[ContainerConfig] = set()
+
+ def add(self, name: str, data: str, target: str, mode: str = ""):
+ self.top_level_configs.add(name, data)
+
+ if target == "":
+ raise RenderError(f"Expected [target] to be set for config [{name}]")
+ if mode != "":
+ mode = valid_octal_mode_or_raise(mode)
+
+ if target in [c.target for c in self.container_configs]:
+ raise RenderError(f"Target [{target}] already used for another config")
+ target = valid_fs_path_or_raise(target)
+ self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+ def has_configs(self):
+ return bool(self.container_configs)
+
+ def render(self):
+ return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+ def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+ self._render_instance = render_instance
+ self.source = source
+ self.target = target
+ self.mode = mode
+
+ def render(self):
+ result: dict[str, str | int] = {
+ "source": self.source,
+ "target": self.target,
+ }
+
+ if self.mode:
+ result["mode"] = int(self.mode, 8)
+
+ return result
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/container.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/container.py
new file mode 100644
index 0000000000..8e889be045
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/container.py
@@ -0,0 +1,418 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .configs import ContainerConfigs
+ from .depends import Depends
+ from .deploy import Deploy
+ from .device_cgroup_rules import DeviceCGroupRules
+ from .devices import Devices
+ from .dns import Dns
+ from .environment import Environment
+ from .error import RenderError
+ from .expose import Expose
+ from .extra_hosts import ExtraHosts
+ from .formatter import escape_dollar, get_image_with_hashed_data
+ from .healthcheck import Healthcheck
+ from .labels import Labels
+ from .ports import Ports
+ from .restart import RestartPolicy
+ from .validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from .storage import Storage
+ from .sysctls import Sysctls
+except ImportError:
+ from configs import ContainerConfigs
+ from depends import Depends
+ from deploy import Deploy
+ from device_cgroup_rules import DeviceCGroupRules
+ from devices import Devices
+ from dns import Dns
+ from environment import Environment
+ from error import RenderError
+ from expose import Expose
+ from extra_hosts import ExtraHosts
+ from formatter import escape_dollar, get_image_with_hashed_data
+ from healthcheck import Healthcheck
+ from labels import Labels
+ from ports import Ports
+ from restart import RestartPolicy
+ from validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from storage import Storage
+ from sysctls import Sysctls
+
+
+class Container:
+ def __init__(self, render_instance: "Render", name: str, image: str):
+ self._render_instance = render_instance
+
+ self._name: str = name
+ self._image: str = self._resolve_image(image)
+ self._build_image: str = ""
+ self._pull_policy: str = ""
+ self._user: str = ""
+ self._tty: bool = False
+ self._stdin_open: bool = False
+ self._init: bool | None = None
+ self._read_only: bool | None = None
+ self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+ self._hostname: str = ""
+ self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
+ self._cap_add: set[str] = set()
+ self._security_opt: set[str] = set(["no-new-privileges"])
+ self._privileged: bool = False
+ self._group_add: set[int | str] = set()
+ self._network_mode: str = ""
+ self._entrypoint: list[str] = []
+ self._command: list[str] = []
+ self._grace_period: int | None = None
+ self._shm_size: int | None = None
+ self._storage: Storage = Storage(self._render_instance)
+ self._ipc_mode: str | None = None
+ self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+ self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+ self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+ self.deploy: Deploy = Deploy(self._render_instance)
+ self.networks: set[str] = set()
+ self.devices: Devices = Devices(self._render_instance)
+ self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+ self.dns: Dns = Dns(self._render_instance)
+ self.depends: Depends = Depends(self._render_instance)
+ self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+ self.labels: Labels = Labels(self._render_instance)
+ self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+ self.ports: Ports = Ports(self._render_instance)
+ self.expose: Expose = Expose(self._render_instance)
+
+ self._auto_set_network_mode()
+ self._auto_add_labels()
+ self._auto_add_groups()
+
+ def _auto_add_groups(self):
+ self.add_group(568)
+
+ def _auto_set_network_mode(self):
+ if self._render_instance.values.get("network", {}).get("host_network", False):
+ self.set_network_mode("host")
+
+ def _auto_add_labels(self):
+ labels = self._render_instance.values.get("labels", [])
+ if not labels:
+ return
+
+ for label in labels:
+ containers = label.get("containers", [])
+ if not containers:
+ raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+ if self._name in containers:
+ self.labels.add_label(label["key"], label["value"])
+
+ def _resolve_image(self, image: str):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(
+ f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+ )
+ repo = images[image].get("repository", "")
+ tag = images[image].get("tag", "")
+
+ if not repo:
+ raise RenderError(f"Repository not found for image [{image}]")
+ if not tag:
+ raise RenderError(f"Tag not found for image [{image}]")
+
+ return f"{repo}:{tag}"
+
+ def build_image(self, content: list[str | None]):
+ dockerfile = f"FROM {self._image}\n"
+ for line in content:
+ if not line:
+ continue
+ if line.startswith("FROM"):
+ # TODO: This will also block multi-stage builds
+ # We can revisit this later if we need it
+ raise RenderError(
+ "FROM cannot be used in build image. Define the base image when creating the container."
+ )
+ dockerfile += line + "\n"
+
+ self._build_image = dockerfile
+ self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+ def set_pull_policy(self, pull_policy: str):
+ self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+ def set_user(self, user: int, group: int):
+ for i in (user, group):
+ if not isinstance(i, int) or i < 0:
+ raise RenderError(f"User/Group [{i}] is not valid")
+ self._user = f"{user}:{group}"
+
+ def add_extra_host(self, host: str, ip: str):
+ self._extra_hosts.add_host(host, ip)
+
+ def add_group(self, group: int | str):
+ if isinstance(group, str):
+ group = str(group).strip()
+ if group.isdigit():
+ raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+ if group in self._group_add:
+ raise RenderError(f"Group [{group}] already added")
+ self._group_add.add(group)
+
+ def get_additional_groups(self) -> list[int | str]:
+ result = []
+ if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+ result.append(44) # video
+ result.append(107) # render
+ return result
+
+ def get_current_groups(self) -> list[str]:
+ result = [str(g) for g in self._group_add]
+ result.extend([str(g) for g in self.get_additional_groups()])
+ return result
+
+ def set_tty(self, enabled: bool = False):
+ self._tty = enabled
+
+ def set_stdin(self, enabled: bool = False):
+ self._stdin_open = enabled
+
+ def set_ipc_mode(self, ipc_mode: str):
+ self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+ def add_device_cgroup_rule(self, dev_grp_rule: str):
+ self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+ def set_init(self, enabled: bool = False):
+ self._init = enabled
+
+ def set_read_only(self, enabled: bool = False):
+ self._read_only = enabled
+
+ def set_hostname(self, hostname: str):
+ self._hostname = hostname
+
+ def set_grace_period(self, grace_period: int):
+ if grace_period < 0:
+ raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+ self._grace_period = grace_period
+
+ def set_privileged(self, enabled: bool = False):
+ self._privileged = enabled
+
+ def clear_caps(self):
+ self._cap_add.clear()
+ self._cap_drop.clear()
+
+ def add_caps(self, caps: list[str]):
+ for c in caps:
+ if c in self._cap_add:
+ raise RenderError(f"Capability [{c}] already added")
+ self._cap_add.add(valid_cap_or_raise(c))
+
+ def add_security_opt(self, opt: str):
+ if opt in self._security_opt:
+ raise RenderError(f"Security Option [{opt}] already added")
+ self._security_opt.add(opt)
+
+ def remove_security_opt(self, opt: str):
+ self._security_opt.remove(opt)
+
+ def set_network_mode(self, mode: str):
+ self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+ def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+ port_config = port_config or {}
+ dev_config = dev_config or {}
+ # Merge port_config and dev_config (dev_config has precedence)
+ config = port_config | dev_config
+
+ bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+ # Skip port if its neither published nor exposed
+ if not bind_mode:
+ return
+
+ # Collect port config
+ host_port = config.get("port_number", 0)
+ container_port = config.get("container_port", 0) or host_port
+ protocol = config.get("protocol", "tcp")
+ host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+ if not isinstance(host_ips, list):
+ raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+ if bind_mode == "published":
+ for host_ip in host_ips:
+ self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip})
+ elif bind_mode == "exposed":
+ self.expose.add_port(container_port, protocol)
+
+ def set_entrypoint(self, entrypoint: list[str]):
+ self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+ def set_command(self, command: list[str]):
+ self._command = [escape_dollar(str(e)) for e in command]
+
+ def add_storage(self, mount_path: str, config: "IxStorage"):
+ self._storage.add(mount_path, config)
+
+ def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+ self.add_group(999)
+ self._storage._add_docker_socket(read_only, mount_path)
+
+ def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+ self._storage._add_udev(read_only, mount_path)
+
+ def add_tun_device(self):
+ self.devices._add_tun_device()
+
+ def add_snd_device(self):
+ self.add_group(29)
+ self.devices._add_snd_device()
+
+ def set_shm_size_mb(self, size: int):
+ self._shm_size = size
+
+ # Easily remove devices from the container
+ # Useful in dependencies like postgres and redis
+ # where there is no need to pass devices to them
+ def remove_devices(self):
+ self.deploy.resources.remove_devices()
+ self.devices.remove_devices()
+
+ @property
+ def storage(self):
+ return self._storage
+
+ def render(self) -> dict[str, Any]:
+ if self._network_mode and self.networks:
+ raise RenderError("Cannot set both [network_mode] and [networks]")
+
+ result = {
+ "image": self._image,
+ "platform": "linux/amd64",
+ "tty": self._tty,
+ "stdin_open": self._stdin_open,
+ "restart": self.restart.render(),
+ }
+
+ if self._pull_policy:
+ result["pull_policy"] = self._pull_policy
+
+ if self.healthcheck.has_healthcheck():
+ result["healthcheck"] = self.healthcheck.render()
+
+ if self._hostname:
+ result["hostname"] = self._hostname
+
+ if self._build_image:
+ result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ if self._ipc_mode is not None:
+ result["ipc"] = self._ipc_mode
+
+ if self._device_cgroup_rules.has_rules():
+ result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+ if self._extra_hosts.has_hosts():
+ result["extra_hosts"] = self._extra_hosts.render()
+
+ if self._init is not None:
+ result["init"] = self._init
+
+ if self._read_only is not None:
+ result["read_only"] = self._read_only
+
+ if self._grace_period is not None:
+ result["stop_grace_period"] = f"{self._grace_period}s"
+
+ if self._user:
+ result["user"] = self._user
+
+ for g in self.get_additional_groups():
+ self.add_group(g)
+
+ if self._group_add:
+ result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+ if self._shm_size is not None:
+ result["shm_size"] = f"{self._shm_size}M"
+
+ if self._privileged is not None:
+ result["privileged"] = self._privileged
+
+ if self._cap_drop:
+ result["cap_drop"] = sorted(self._cap_drop)
+
+ if self._cap_add:
+ result["cap_add"] = sorted(self._cap_add)
+
+ if self._security_opt:
+ result["security_opt"] = sorted(self._security_opt)
+
+ if self._network_mode:
+ result["network_mode"] = self._network_mode
+
+ if self.sysctls.has_sysctls():
+ result["sysctls"] = self.sysctls.render()
+
+ if self._network_mode != "host":
+ if self.ports.has_ports():
+ result["ports"] = self.ports.render()
+
+ if self.expose.has_ports():
+ result["expose"] = self.expose.render()
+
+ if self._entrypoint:
+ result["entrypoint"] = self._entrypoint
+
+ if self._command:
+ result["command"] = self._command
+
+ if self.devices.has_devices():
+ result["devices"] = self.devices.render()
+
+ if self.deploy.has_deploy():
+ result["deploy"] = self.deploy.render()
+
+ if self.environment.has_variables():
+ result["environment"] = self.environment.render()
+
+ if self.labels.has_labels():
+ result["labels"] = self.labels.render()
+
+ if self.dns.has_dns_nameservers():
+ result["dns"] = self.dns.render_dns_nameservers()
+
+ if self.dns.has_dns_searches():
+ result["dns_search"] = self.dns.render_dns_searches()
+
+ if self.dns.has_dns_opts():
+ result["dns_opt"] = self.dns.render_dns_opts()
+
+ if self.depends.has_dependencies():
+ result["depends_on"] = self.depends.render()
+
+ if self._storage.has_mounts():
+ result["volumes"] = self._storage.render()
+
+ return result
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/depends.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/depends.py
new file mode 100644
index 0000000000..4e057cf085
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/depends.py
@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_depend_condition_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dependencies: dict[str, str] = {}
+
+ def add_dependency(self, name: str, condition: str):
+ condition = valid_depend_condition_or_raise(condition)
+ if name in self._dependencies.keys():
+ raise RenderError(f"Dependency [{name}] already added")
+ if name not in self._render_instance.container_names():
+ raise RenderError(
+ f"Dependency [{name}] not found in defined containers. "
+ f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+ )
+ self._dependencies[name] = condition
+
+ def has_dependencies(self):
+ return len(self._dependencies) > 0
+
+ def render(self):
+ return {d: {"condition": c} for d, c in self._dependencies.items()}
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deploy.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deploy.py
new file mode 100644
index 0000000000..894dbc643b
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deploy.py
@@ -0,0 +1,24 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .resources import Resources
+except ImportError:
+ from resources import Resources
+
+
+class Deploy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self.resources: Resources = Resources(self._render_instance)
+
+ def has_deploy(self):
+ return self.resources.has_resources()
+
+ def render(self):
+ if self.resources.has_resources():
+ return {"resources": self.resources.render()}
+
+ return {}
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps.py
new file mode 100644
index 0000000000..e96849f979
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps.py
@@ -0,0 +1,32 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .deps_postgres import PostgresContainer, PostgresConfig
+ from .deps_redis import RedisContainer, RedisConfig
+ from .deps_mariadb import MariadbContainer, MariadbConfig
+ from .deps_perms import PermsContainer
+except ImportError:
+ from deps_postgres import PostgresContainer, PostgresConfig
+ from deps_redis import RedisContainer, RedisConfig
+ from deps_mariadb import MariadbContainer, MariadbConfig
+ from deps_perms import PermsContainer
+
+
+class Deps:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def perms(self, name: str):
+ return PermsContainer(self._render_instance, name)
+
+ def postgres(self, name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer):
+ return PostgresContainer(self._render_instance, name, image, config, perms_instance)
+
+ def redis(self, name: str, image: str, config: RedisConfig, perms_instance: PermsContainer):
+ return RedisContainer(self._render_instance, name, image, config, perms_instance)
+
+ def mariadb(self, name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer):
+ return MariadbContainer(self._render_instance, name, image, config, perms_instance)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_mariadb.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_mariadb.py
new file mode 100644
index 0000000000..baf863b370
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_mariadb.py
@@ -0,0 +1,65 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class MariadbConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ root_password: NotRequired[str]
+ port: NotRequired[int]
+ auto_upgrade: NotRequired[bool]
+ volume: "IxStorage"
+
+
+class MariadbContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for mariadb")
+
+ port = valid_port_or_raise(config.get("port") or 3306)
+ root_password = config.get("root_password") or config["password"]
+ auto_upgrade = config.get("auto_upgrade", True)
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(999, 999)
+ c.healthcheck.set_test("mariadb")
+ c.remove_devices()
+
+ c.add_storage("/var/lib/mysql", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_mariadb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ c.environment.add_env("MARIADB_USER", config["user"])
+ c.environment.add_env("MARIADB_PASSWORD", config["password"])
+ c.environment.add_env("MARIADB_ROOT_PASSWORD", root_password)
+ c.environment.add_env("MARIADB_DATABASE", config["database"])
+ c.environment.add_env("MARIADB_AUTO_UPGRADE", str(auto_upgrade).lower())
+ c.set_command(["--port", str(port)])
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_perms.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_perms.py
new file mode 100644
index 0000000000..cdc5a3820a
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_perms.py
@@ -0,0 +1,252 @@
+import json
+import pathlib
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class PermsContainer:
+ def __init__(self, render_instance: "Render", name: str):
+ self._render_instance = render_instance
+ self._name = name
+ self.actions: set[str] = set()
+ self.parsed_configs: list[dict] = []
+
+ def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ identifier = self.normalize_identifier_for_path(identifier)
+ if identifier in self.actions:
+ raise RenderError(f"Action with id [{identifier}] already used for another permission action")
+
+ parsed_action = self.parse_action(identifier, volume_config, action_config)
+ if parsed_action:
+ self.parsed_configs.append(parsed_action)
+ self.actions.add(identifier)
+
+ def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ valid_modes = [
+ "always", # Always set permissions, without checking.
+ "check", # Checks if permissions are correct, and set them if not.
+ ]
+ mode = action_config.get("mode", "check")
+ uid = action_config.get("uid", None)
+ gid = action_config.get("gid", None)
+ chmod = action_config.get("chmod", None)
+ recursive = action_config.get("recursive", False)
+ mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
+ is_temporary = False
+
+ vol_type = volume_config.get("type", "")
+ match vol_type:
+ case "temporary":
+ # If it is a temporary volume, we force auto permissions
+ # and set is_temporary to True, so it will be cleaned up
+ is_temporary = True
+ recursive = True
+ case "volume":
+ if not volume_config.get("volume_config", {}).get("auto_permissions", False):
+ return None
+ case "host_path":
+ host_path_config = volume_config.get("host_path_config", {})
+ # Skip when ACL enabled
+ if host_path_config.get("acl_enable", False):
+ return None
+ if not host_path_config.get("auto_permissions", False):
+ return None
+ case "ix_volume":
+ ix_vol_config = volume_config.get("ix_volume_config", {})
+ # Skip when ACL enabled
+ if ix_vol_config.get("acl_enable", False):
+ return None
+ # For ix_volumes, we default to auto_permissions = True
+ if not ix_vol_config.get("auto_permissions", True):
+ return None
+ case _:
+ # Skip for other types
+ return None
+
+ if mode not in valid_modes:
+ raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
+ if not isinstance(uid, int) or not isinstance(gid, int):
+ raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
+ if chmod is not None:
+ chmod = valid_octal_mode_or_raise(chmod)
+
+ mount_path = valid_fs_path_or_raise(mount_path)
+ return {
+ "mount_path": mount_path,
+ "volume_config": volume_config,
+ "action_data": {
+ "mount_path": mount_path,
+ "is_temporary": is_temporary,
+ "identifier": identifier,
+ "recursive": recursive,
+ "mode": mode,
+ "uid": uid,
+ "gid": gid,
+ "chmod": chmod,
+ },
+ }
+
+ def normalize_identifier_for_path(self, identifier: str):
+ return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
+
+ def has_actions(self):
+ return bool(self.actions)
+
+ def activate(self):
+ if len(self.parsed_configs) != len(self.actions):
+ raise RenderError("Number of actions and parsed configs does not match")
+
+ if not self.has_actions():
+ raise RenderError("No actions added. Check if there are actions before activating")
+
+ # Add the container and set it up
+ c = self._render_instance.add_container(self._name, "python_permissions_image")
+ c.set_user(0, 0)
+ c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
+ c.set_network_mode("none")
+
+ # Don't attach any devices
+ c.remove_devices()
+
+ c.deploy.resources.set_profile("medium")
+ c.restart.set_policy("on-failure", maximum_retry_count=1)
+ c.healthcheck.disable()
+
+ c.set_entrypoint(["python3", "/script/run.py"])
+ script = "#!/usr/bin/env python3\n"
+ script += get_script()
+ c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
+
+ actions_data: list[dict] = []
+ for parsed in self.parsed_configs:
+ c.add_storage(parsed["mount_path"], parsed["volume_config"])
+ actions_data.append(parsed["action_data"])
+
+ actions_data_json = json.dumps(actions_data)
+ c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
+
+
+def get_script():
+ return """
+import os
+import json
+import time
+import shutil
+
+with open("/script/actions.json", "r") as f:
+ actions_data = json.load(f)
+
+if not actions_data:
+ # If this script is called, there should be actions data
+ raise ValueError("No actions data found")
+
+def fix_perms(path, chmod, recursive=False):
+ print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
+ os.chmod(path, int(chmod, 8))
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chmod(os.path.join(root, f), int(chmod, 8))
+ print("Permissions after changes:")
+ print_chmod_stat()
+
+def fix_owner(path, uid, gid, recursive=False):
+ print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
+ os.chown(path, uid, gid)
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chown(os.path.join(root, f), uid, gid)
+ print("Ownership after changes:")
+ print_chown_stat()
+
+def print_chown_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
+
+def print_chmod_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
+
+def print_chown_diff(curr_stat, uid, gid):
+ print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
+
+def print_chmod_diff(curr_stat, mode):
+ print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
+
+def perform_action(action):
+ start_time = time.time()
+ print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
+
+ if not os.path.isdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not a directory, skipping...")
+ return
+
+ if action["is_temporary"]:
+ print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
+ for item in os.listdir(action["mount_path"]):
+ item_path = os.path.join(action["mount_path"], item)
+
+ # Exclude the safe directory, where we can use to mount files temporarily
+ if os.path.basename(item_path) == "ix-safe":
+ continue
+ if os.path.isdir(item_path):
+ shutil.rmtree(item_path)
+ else:
+ os.remove(item_path)
+
+ if not action["is_temporary"] and os.listdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not empty, skipping...")
+ return
+
+ print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
+ curr_stat = os.stat(action["mount_path"])
+ print_chown_diff(curr_stat, action["uid"], action["gid"])
+ print_chmod_diff(curr_stat, action["chmod"])
+ print("---")
+
+ if action["mode"] == "always":
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ return
+
+ elif action["mode"] == "check":
+ if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
+ print("Ownership is incorrect. Fixing...")
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ else:
+ print("Ownership is correct. Skipping...")
+
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ if oct(curr_stat.st_mode)[3:] != action["chmod"]:
+ print("Permissions are incorrect. Fixing...")
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ else:
+ print("Permissions are correct. Skipping...")
+
+ print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
+ print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
+ print()
+
+if __name__ == "__main__":
+ start_time = time.time()
+ for action in actions_data:
+ perform_action(action)
+ print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
+"""
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_postgres.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_postgres.py
new file mode 100644
index 0000000000..c40e900d07
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_postgres.py
@@ -0,0 +1,279 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class PostgresConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+MAX_POSTGRES_VERSION = 17
+
+
+class PostgresContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+ self._data_dir = "/var/lib/postgresql/data"
+ self._upgrade_name = f"{self._name}_upgrade"
+ self._upgrade_container = None
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for postgres")
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+
+ c.set_user(999, 999)
+ c.healthcheck.set_test("postgres")
+ c.remove_devices()
+ c.add_storage(self._data_dir, config["volume"])
+
+ common_variables = {
+ "POSTGRES_USER": config["user"],
+ "POSTGRES_PASSWORD": config["password"],
+ "POSTGRES_DB": config["database"],
+ "POSTGRES_PORT": port,
+ }
+
+ for k, v in common_variables.items():
+ c.environment.add_env(k, v)
+
+ perms_instance.add_or_skip_action(
+ f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ repo = self._get_repo(image)
+ # eg we don't want to handle upgrades of pg_vector at the moment
+ if repo == "postgres":
+ target_major_version = self._get_target_version(image)
+ upg = self._render_instance.add_container(self._upgrade_name, image)
+ upg.build_image(get_build_manifest())
+ upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
+ upg.configs.add("pg_container_upgrade.sh", get_upgrade_script(), "/upgrade.sh", "0755")
+ upg.restart.set_policy("on-failure", 1)
+ upg.set_user(999, 999)
+ upg.healthcheck.disable()
+ upg.remove_devices()
+ upg.add_storage(self._data_dir, config["volume"])
+ for k, v in common_variables.items():
+ upg.environment.add_env(k, v)
+
+ upg.environment.add_env("TARGET_VERSION", target_major_version)
+ upg.environment.add_env("DATA_DIR", self._data_dir)
+
+ self._upgrade_container = upg
+
+ c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
+
+ def add_dependency(self, container_name: str, condition: str):
+ self._container.depends.add_dependency(container_name, condition)
+ if self._upgrade_container:
+ self._upgrade_container.depends.add_dependency(container_name, condition)
+
+ def _get_port(self):
+ return self._config.get("port") or 5432
+
+ def _get_repo(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ repo = images[image].get("repository", "")
+ if not repo:
+ raise RenderError("Could not determine repo")
+ return repo
+
+ def _get_target_version(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ tag = images[image].get("tag", "")
+ tag = str(tag) # Account for tags like 16.6
+ target_major_version = tag.split(".")[0]
+
+ try:
+ target_major_version = int(target_major_version)
+ except ValueError:
+ raise RenderError(f"Could not determine target major version from tag [{tag}]")
+
+ if target_major_version > MAX_POSTGRES_VERSION:
+ raise RenderError(f"Postgres version [{target_major_version}] is not supported")
+
+ return target_major_version
+
+ def get_url(self, variant: str):
+ user = urllib.parse.quote_plus(self._config["user"])
+ password = urllib.parse.quote_plus(self._config["password"])
+ creds = f"{user}:{password}"
+ addr = f"{self._name}:{self._get_port()}"
+ db = self._config["database"]
+
+ match variant:
+ case "postgres":
+ return f"postgres://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql":
+ return f"postgresql://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql_no_creds":
+ return f"postgresql://{addr}/{db}?sslmode=disable"
+ case "host_port":
+ return addr
+ case _:
+ raise RenderError(f"Expected [variant] to be one of [postgres, postgresql], got [{variant}]")
+
+
+def get_build_manifest() -> list[str | None]:
+ return [
+ f"RUN apt-get update && apt-get install -y {' '.join(get_upgrade_packages())}",
+ "WORKDIR /tmp",
+ ]
+
+
+def get_upgrade_packages():
+ return [
+ "rsync",
+ "postgresql-13",
+ "postgresql-14",
+ "postgresql-15",
+ "postgresql-16",
+ ]
+
+
+def get_upgrade_script():
+ return """
+#!/bin/bash
+set -euo pipefail
+
+get_bin_path() {
+ local version=$1
+ echo "/usr/lib/postgresql/$version/bin"
+}
+
+log() {
+ echo "[ix-postgres-upgrade] - [$(date +'%Y-%m-%d %H:%M:%S')] - $1"
+}
+
+check_writable() {
+ local path=$1
+ if [ ! -w "$path" ]; then
+ log "$path is not writable"
+ exit 1
+ fi
+}
+
+check_writable "$DATA_DIR"
+
+# Don't do anything if its a fresh install.
+if [ ! -f "$DATA_DIR/PG_VERSION" ]; then
+ log "File $DATA_DIR/PG_VERSION does not exist. Assuming this is a fresh install."
+ exit 0
+fi
+
+# Don't do anything if we're already at the target version.
+OLD_VERSION=$(cat "$DATA_DIR/PG_VERSION")
+log "Current version: $OLD_VERSION"
+log "Target version: $TARGET_VERSION"
+if [ "$OLD_VERSION" -eq "$TARGET_VERSION" ]; then
+ log "Already at target version $TARGET_VERSION"
+ exit 0
+fi
+
+# Fail if we're downgrading.
+if [ "$OLD_VERSION" -gt "$TARGET_VERSION" ]; then
+ log "Cannot downgrade from $OLD_VERSION to $TARGET_VERSION"
+ exit 1
+fi
+
+export OLD_PG_BINARY=$(get_bin_path "$OLD_VERSION")
+if [ ! -f "$OLD_PG_BINARY/pg_upgrade" ]; then
+ log "File $OLD_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_PG_BINARY=$(get_bin_path "$TARGET_VERSION")
+if [ ! -f "$NEW_PG_BINARY/pg_upgrade" ]; then
+ log "File $NEW_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_DATA_DIR="/tmp/new-data-dir"
+if [ -d "$NEW_DATA_DIR" ]; then
+ log "Directory $NEW_DATA_DIR already exists."
+ exit 1
+fi
+
+export PGUSER="$POSTGRES_USER"
+log "Creating new data dir and initializing..."
+PGDATA="$NEW_DATA_DIR" eval "initdb --username=$POSTGRES_USER --pwfile=<(echo $POSTGRES_PASSWORD)"
+
+timestamp=$(date +%Y%m%d%H%M%S)
+backup_name="backup-$timestamp-$OLD_VERSION-$TARGET_VERSION.tar.gz"
+log "Backing up $DATA_DIR to $NEW_DATA_DIR/$backup_name"
+tar -czf "$NEW_DATA_DIR/$backup_name" "$DATA_DIR"
+
+log "Using old pg_upgrade [$OLD_PG_BINARY/pg_upgrade]"
+log "Using new pg_upgrade [$NEW_PG_BINARY/pg_upgrade]"
+log "Checking upgrade compatibility of $OLD_VERSION to $TARGET_VERSION..."
+
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql \
+ --check
+
+log "Compatibility check passed."
+
+log "Upgrading from $OLD_VERSION to $TARGET_VERSION..."
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql
+
+log "Upgrade complete."
+
+log "Copying old pg_hba.conf to new pg_hba.conf"
+# We need to carry this over otherwise
+cp "$DATA_DIR/pg_hba.conf" "$NEW_DATA_DIR/pg_hba.conf"
+
+log "Replacing contents of $DATA_DIR with contents of $NEW_DATA_DIR (including the backup)."
+rsync --archive --delete "$NEW_DATA_DIR/" "$DATA_DIR/"
+
+log "Removing $NEW_DATA_DIR."
+rm -rf "$NEW_DATA_DIR"
+
+log "Done."
+"""
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_redis.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_redis.py
new file mode 100644
index 0000000000..d49ebe7683
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/deps_redis.py
@@ -0,0 +1,71 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise, valid_redis_password_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise, valid_redis_password_or_raise
+
+
+class RedisConfig(TypedDict):
+ password: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+class RedisContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: RedisConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+
+ for key in ("password", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for redis")
+
+ valid_redis_password_or_raise(config["password"])
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(1001, 0)
+ c.healthcheck.set_test("redis")
+ c.remove_devices()
+
+ c.add_storage("/bitnami/redis/data", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_redis_data", config["volume"], {"uid": 1001, "gid": 0, "mode": "check"}
+ )
+
+ c.environment.add_env("ALLOW_EMPTY_PASSWORD", "no")
+ c.environment.add_env("REDIS_PASSWORD", config["password"])
+ c.environment.add_env("REDIS_PORT_NUMBER", port)
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ def _get_port(self):
+ return self._config.get("port") or 6379
+
+ def get_url(self, variant: str):
+ addr = f"{self._name}:{self._get_port()}"
+ password = urllib.parse.quote_plus(self._config["password"])
+
+ match variant:
+ case "redis":
+ return f"redis://default:{password}@{addr}"
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device.py
new file mode 100644
index 0000000000..bfe97097cb
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device.py
@@ -0,0 +1,31 @@
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+
+
+class Device:
+ def __init__(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ hd = valid_fs_path_or_raise(host_device.rstrip("/"))
+ cd = valid_fs_path_or_raise(container_device.rstrip("/"))
+ if not hd or not cd:
+ raise RenderError(
+ "Expected [host_device] and [container_device] to be set. "
+ f"Got host_device [{host_device}] and container_device [{container_device}]"
+ )
+
+ cgroup_perm = valid_cgroup_perm_or_raise(cgroup_perm)
+ if not allow_disallowed:
+ hd = allowed_device_or_raise(hd)
+
+ self.cgroup_perm: str = cgroup_perm
+ self.host_device: str = hd
+ self.container_device: str = cd
+
+ def render(self):
+ result = f"{self.host_device}:{self.container_device}"
+ if self.cgroup_perm:
+ result += f":{self.cgroup_perm}"
+ return result
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device_cgroup_rules.py
new file mode 100644
index 0000000000..dcccfee773
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/device_cgroup_rules.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+ def __init__(self, rule: str):
+ rule = valid_device_cgroup_rule_or_raise(rule)
+ parts = rule.split(" ")
+ major, minor = parts[1].split(":")
+
+ self._type = parts[0]
+ self._major = major
+ self._minor = minor
+ self._permissions = parts[2]
+
+ def get_key(self):
+ return f"{self._type}_{self._major}_{self._minor}"
+
+ def render(self):
+ return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._rules: set[DeviceCGroupRule] = set()
+ self._track_rule_combos: set[str] = set()
+
+ def add_rule(self, rule: str):
+ dev_group_rule = DeviceCGroupRule(rule)
+ if dev_group_rule in self._rules:
+ raise RenderError(f"Device Group Rule [{rule}] already added")
+
+ rule_key = dev_group_rule.get_key()
+ if rule_key in self._track_rule_combos:
+ raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+ self._rules.add(dev_group_rule)
+ self._track_rule_combos.add(rule_key)
+
+ def has_rules(self):
+ return len(self._rules) > 0
+
+ def render(self):
+ return sorted([rule.render() for rule in self._rules])
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/devices.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/devices.py
new file mode 100644
index 0000000000..168e98d032
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/devices.py
@@ -0,0 +1,71 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .device import Device
+except ImportError:
+ from error import RenderError
+ from device import Device
+
+
+class Devices:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._devices: set[Device] = set()
+
+ # Tracks all container device paths to make sure they are not duplicated
+ self._container_device_paths: set[str] = set()
+ # Scan values for devices we should automatically add
+ # for example /dev/dri for gpus
+ self._auto_add_devices_from_values()
+
+ def _auto_add_devices_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+
+ if resources.get("gpus", {}).get("use_all_gpus", False):
+ self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
+ if resources["gpus"].get("kfd_device_exists", False):
+ self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
+
+ def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ # Host device can be mapped to multiple container devices,
+ # so we only make sure container devices are not duplicated
+ if container_device in self._container_device_paths:
+ raise RenderError(f"Device with container path [{container_device}] already added")
+
+ self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
+ self._container_device_paths.add(container_device)
+
+ def add_usb_bus(self):
+ self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
+
+ def _add_snd_device(self):
+ self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
+
+ def _add_tun_device(self):
+ self.add_device("/dev/net/tun", "/dev/net/tun", allow_disallowed=True)
+
+ def has_devices(self):
+ return len(self._devices) > 0
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._devices.clear()
+ self._container_device_paths.clear()
+
+ # Check if there are any gpu devices
+ # Used to determine if we should add groups
+ # like 'video' to the container
+ def has_gpus(self):
+ for d in self._devices:
+ if d.host_device == "/dev/dri":
+ return True
+ return False
+
+ def render(self) -> list[str]:
+ return sorted([d.render() for d in self._devices])
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/dns.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/dns.py
new file mode 100644
index 0000000000..d3ae7b19fa
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/dns.py
@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import allowed_dns_opt_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import allowed_dns_opt_or_raise
+
+
+class Dns:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dns_options: set[str] = set()
+ self._dns_searches: set[str] = set()
+ self._dns_nameservers: set[str] = set()
+
+ self._auto_add_dns_opts_from_values()
+ self._auto_add_dns_searches_from_values()
+ self._auto_add_dns_nameservers_from_values()
+
+ def _get_dns_opt_keys(self):
+ return [self._get_key_from_opt(opt) for opt in self._dns_options]
+
+ def _get_key_from_opt(self, opt):
+ return opt.split(":")[0]
+
+ def _auto_add_dns_opts_from_values(self):
+ values = self._render_instance.values
+ for dns_opt in values.get("network", {}).get("dns_opts", []):
+ self.add_dns_opt(dns_opt)
+
+ def _auto_add_dns_searches_from_values(self):
+ values = self._render_instance.values
+ for dns_search in values.get("network", {}).get("dns_searches", []):
+ self.add_dns_search(dns_search)
+
+ def _auto_add_dns_nameservers_from_values(self):
+ values = self._render_instance.values
+ for dns_nameserver in values.get("network", {}).get("dns_nameservers", []):
+ self.add_dns_nameserver(dns_nameserver)
+
+ def add_dns_search(self, dns_search):
+ if dns_search in self._dns_searches:
+ raise RenderError(f"DNS Search [{dns_search}] already added")
+ self._dns_searches.add(dns_search)
+
+ def add_dns_nameserver(self, dns_nameserver):
+ if dns_nameserver in self._dns_nameservers:
+ raise RenderError(f"DNS Nameserver [{dns_nameserver}] already added")
+ self._dns_nameservers.add(dns_nameserver)
+
+ def add_dns_opt(self, dns_opt):
+ # eg attempts:3
+ key = allowed_dns_opt_or_raise(self._get_key_from_opt(dns_opt))
+ if key in self._get_dns_opt_keys():
+ raise RenderError(f"DNS Option [{key}] already added")
+ self._dns_options.add(dns_opt)
+
+ def has_dns_opts(self):
+ return len(self._dns_options) > 0
+
+ def has_dns_searches(self):
+ return len(self._dns_searches) > 0
+
+ def has_dns_nameservers(self):
+ return len(self._dns_nameservers) > 0
+
+ def render_dns_searches(self):
+ return sorted(self._dns_searches)
+
+ def render_dns_opts(self):
+ return sorted(self._dns_options)
+
+ def render_dns_nameservers(self):
+ return sorted(self._dns_nameservers)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/environment.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/environment.py
new file mode 100644
index 0000000000..056763ea80
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/environment.py
@@ -0,0 +1,112 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .resources import Resources
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from resources import Resources
+
+
+class Environment:
+ def __init__(self, render_instance: "Render", resources: Resources):
+ self._render_instance = render_instance
+ self._resources = resources
+ # Stores variables that user defined
+ self._user_vars: dict[str, Any] = {}
+ # Stores variables that are automatically added (based on values)
+ self._auto_variables: dict[str, Any] = {}
+ # Stores variables that are added by the application developer
+ self._app_dev_variables: dict[str, Any] = {}
+
+ self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
+
+ self._auto_add_variables_from_values()
+
+ def _auto_add_variables_from_values(self):
+ if not self._skip_generic_variables:
+ self._add_generic_variables()
+ self._add_nvidia_variables()
+
+ def _add_generic_variables(self):
+ self._auto_variables["TZ"] = self._render_instance.values.get("TZ", "Etc/UTC")
+ self._auto_variables["UMASK"] = self._render_instance.values.get("UMASK", "002")
+ self._auto_variables["UMASK_SET"] = self._render_instance.values.get("UMASK", "002")
+
+ run_as = self._render_instance.values.get("run_as", {})
+ user = run_as.get("user")
+ group = run_as.get("group")
+ if user:
+ self._auto_variables["PUID"] = user
+ self._auto_variables["UID"] = user
+ self._auto_variables["USER_ID"] = user
+ if group:
+ self._auto_variables["PGID"] = group
+ self._auto_variables["GID"] = group
+ self._auto_variables["GROUP_ID"] = group
+
+ def _add_nvidia_variables(self):
+ if self._resources._nvidia_ids:
+ self._auto_variables["NVIDIA_DRIVER_CAPABILITIES"] = "all"
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(sorted(self._resources._nvidia_ids))
+ else:
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = "void"
+
+ def _format_value(self, v: Any) -> str:
+ value = str(v)
+
+ # str(bool) returns "True" or "False",
+ # but we want "true" or "false"
+ if isinstance(v, bool):
+ value = value.lower()
+ return value
+
+ def add_env(self, name: str, value: Any):
+ if not name:
+ raise RenderError(f"Environment variable name cannot be empty. [{name}]")
+ if name in self._app_dev_variables.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{name}] in application developer environment variables."
+ )
+ self._app_dev_variables[name] = value
+
+ def add_user_envs(self, user_env: list[dict]):
+ for item in user_env:
+ if not item.get("name"):
+ raise RenderError(f"Environment variable name cannot be empty. [{item}]")
+ if item["name"] in self._user_vars.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{item['name']}] in user environment variables."
+ )
+ self._user_vars[item["name"]] = item.get("value")
+
+ def has_variables(self):
+ return len(self._auto_variables) > 0 or len(self._user_vars) > 0 or len(self._app_dev_variables) > 0
+
+ def render(self):
+ result: dict[str, str] = {}
+
+ # Add envs from auto variables
+ result.update({k: self._format_value(v) for k, v in self._auto_variables.items()})
+
+ # Track defined keys for faster lookup
+ defined_keys = set(result.keys())
+
+ # Add envs from application developer (prohibit overwriting auto variables)
+ for k, v in self._app_dev_variables.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined automatically from the library.")
+ result[k] = self._format_value(v)
+ defined_keys.add(k)
+
+ # Add envs from user (prohibit overwriting app developer envs and auto variables)
+ for k, v in self._user_vars.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined from the application developer.")
+ result[k] = self._format_value(v)
+
+ return {k: escape_dollar(v) for k, v in result.items()}
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/error.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/error.py
new file mode 100644
index 0000000000..aef48d3b02
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/error.py
@@ -0,0 +1,4 @@
+class RenderError(Exception):
+ """Base class for exceptions in this module."""
+
+ pass
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/expose.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/expose.py
new file mode 100644
index 0000000000..a3ac0aec59
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/expose.py
@@ -0,0 +1,31 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_port_or_raise, valid_port_protocol_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_port_or_raise, valid_port_protocol_or_raise
+
+
+class Expose:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: set[str] = set()
+
+ def add_port(self, port: int, protocol: str = "tcp"):
+ port = valid_port_or_raise(port)
+ protocol = valid_port_protocol_or_raise(protocol)
+ key = f"{port}/{protocol}"
+ if key in self._ports:
+ raise RenderError(f"Exposed port [{port}/{protocol}] already added")
+ self._ports.add(key)
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ return sorted(self._ports)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/extra_hosts.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/extra_hosts.py
new file mode 100644
index 0000000000..eaad3bed26
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/extra_hosts.py
@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+
+class ExtraHosts:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._extra_hosts: dict[str, str] = {}
+
+ def add_host(self, host: str, ip: str):
+ if not ip == "host-gateway":
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+ if host in self._extra_hosts:
+ raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+ self._extra_hosts[host] = ip
+
+ def has_hosts(self):
+ return len(self._extra_hosts) > 0
+
+ def render(self):
+ return {host: ip for host, ip in self._extra_hosts.items()}
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/formatter.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/formatter.py
new file mode 100644
index 0000000000..24e882f47a
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/formatter.py
@@ -0,0 +1,26 @@
+import json
+import hashlib
+
+
+def escape_dollar(text: str) -> str:
+ return text.replace("$", "$$")
+
+
+def get_hashed_name_for_volume(prefix: str, config: dict):
+ config_hash = hashlib.sha256(json.dumps(config).encode("utf-8")).hexdigest()
+ return f"{prefix}_{config_hash}"
+
+
+def get_hash_with_prefix(prefix: str, data: str):
+ return f"{prefix}_{hashlib.sha256(data.encode('utf-8')).hexdigest()}"
+
+
+def merge_dicts_no_overwrite(dict1, dict2):
+ overlapping_keys = dict1.keys() & dict2.keys()
+ if overlapping_keys:
+ raise ValueError(f"Merging of dicts failed. Overlapping keys: {overlapping_keys}")
+ return {**dict1, **dict2}
+
+
+def get_image_with_hashed_data(image: str, data: str):
+ return get_hash_with_prefix(f"ix-{image}", data)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/functions.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/functions.py
new file mode 100644
index 0000000000..02c9cd4708
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/functions.py
@@ -0,0 +1,149 @@
+import re
+import copy
+import bcrypt
+import secrets
+from base64 import b64encode
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .volume_sources import HostPathSource, IxVolumeSource
+except ImportError:
+ from error import RenderError
+ from volume_sources import HostPathSource, IxVolumeSource
+
+
+class Functions:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def _bcrypt_hash(self, password):
+ hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+ return hashed
+
+ def _htpasswd(self, username, password):
+ hashed = self._bcrypt_hash(password)
+ return username + ":" + hashed
+
+ def _secure_string(self, length):
+ return secrets.token_urlsafe(length)[:length]
+
+ def _basic_auth(self, username, password):
+ return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
+
+ def _basic_auth_header(self, username, password):
+ return f"Basic {self._basic_auth(username, password)}"
+
+ def _fail(self, message):
+ raise RenderError(message)
+
+ def _camel_case(self, string):
+ return string.title()
+
+ def _auto_cast(self, value):
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return float(value)
+ except ValueError:
+ pass
+
+ if value.lower() in ["true", "false"]:
+ return value.lower() == "true"
+
+ return value
+
+ def _match_regex(self, value, regex):
+ if not re.match(regex, value):
+ return False
+ return True
+
+ def _must_match_regex(self, value, regex):
+ if not self._match_regex(value, regex):
+ raise RenderError(f"Expected [{value}] to match [{regex}]")
+ return value
+
+ def _is_boolean(self, string):
+ return string.lower() in ["true", "false"]
+
+ def _is_number(self, string):
+ try:
+ float(string)
+ return True
+ except ValueError:
+ return False
+
+ def _copy_dict(self, dict):
+ return copy.deepcopy(dict)
+
+ def _merge_dicts(self, *dicts):
+ merged_dict = {}
+ for dictionary in dicts:
+ merged_dict.update(dictionary)
+ return merged_dict
+
+ def _disallow_chars(self, string: str, chars: list[str], key: str):
+ for char in chars:
+ if char in string:
+ raise RenderError(f"Disallowed character [{char}] in [{key}]")
+ return string
+
+ def _or_default(self, value, default):
+ if not value:
+ return default
+ return value
+
+ def _temp_config(self, name):
+ if not name:
+ raise RenderError("Expected [name] to be set when calling [temp_config].")
+ return {"type": "temporary", "volume_config": {"volume_name": name}}
+
+ def _get_host_path(self, storage):
+ source_type = storage.get("type", "")
+ if not source_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match source_type:
+ case "host_path":
+ mount_config = storage.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ host_source = HostPathSource(self._render_instance, mount_config).get()
+ return host_source
+ case "ix_volume":
+ mount_config = storage.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ ix_source = IxVolumeSource(self._render_instance, mount_config).get()
+ return ix_source
+ case _:
+ raise RenderError(f"Storage type [{source_type}] does not support host path.")
+
+ def func_map(self):
+ # TODO: Check what is no longer used and remove
+ return {
+ "auto_cast": self._auto_cast,
+ "basic_auth_header": self._basic_auth_header,
+ "basic_auth": self._basic_auth,
+ "bcrypt_hash": self._bcrypt_hash,
+ "camel_case": self._camel_case,
+ "copy_dict": self._copy_dict,
+ "fail": self._fail,
+ "htpasswd": self._htpasswd,
+ "is_boolean": self._is_boolean,
+ "is_number": self._is_number,
+ "match_regex": self._match_regex,
+ "merge_dicts": self._merge_dicts,
+ "must_match_regex": self._must_match_regex,
+ "secure_string": self._secure_string,
+ "disallow_chars": self._disallow_chars,
+ "get_host_path": self._get_host_path,
+ "or_default": self._or_default,
+ "temp_config": self._temp_config,
+ }
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/healthcheck.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/healthcheck.py
new file mode 100644
index 0000000000..0805329284
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/healthcheck.py
@@ -0,0 +1,203 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_http_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_http_path_or_raise
+
+
+class Healthcheck:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._test: str | list[str] = ""
+ self._interval_sec: int = 10
+ self._timeout_sec: int = 5
+ self._retries: int = 30
+ self._start_period_sec: int = 10
+ self._disabled: bool = False
+ self._use_built_in: bool = False
+
+ def _get_test(self):
+ if isinstance(self._test, str):
+ return escape_dollar(self._test)
+
+ return [escape_dollar(t) for t in self._test]
+
+ def disable(self):
+ self._disabled = True
+
+ def use_built_in(self):
+ self._use_built_in = True
+
+ def set_custom_test(self, test: str | list[str]):
+ if self._disabled:
+ raise RenderError("Cannot set custom test when healthcheck is disabled")
+ self._test = test
+
+ def set_test(self, variant: str, config: dict | None = None):
+ config = config or {}
+ self.set_custom_test(test_mapping(variant, config))
+
+ def set_interval(self, interval: int):
+ self._interval_sec = interval
+
+ def set_timeout(self, timeout: int):
+ self._timeout_sec = timeout
+
+ def set_retries(self, retries: int):
+ self._retries = retries
+
+ def set_start_period(self, start_period: int):
+ self._start_period_sec = start_period
+
+ def has_healthcheck(self):
+ return not self._use_built_in
+
+ def render(self):
+ if self._use_built_in:
+ return RenderError("Should not be called when built in healthcheck is used")
+
+ if self._disabled:
+ return {"disable": True}
+
+ if not self._test:
+ raise RenderError("Healthcheck test is not set")
+
+ return {
+ "test": self._get_test(),
+ "interval": f"{self._interval_sec}s",
+ "timeout": f"{self._timeout_sec}s",
+ "retries": self._retries,
+ "start_period": f"{self._start_period_sec}s",
+ }
+
+
+def test_mapping(variant: str, config: dict | None = None) -> str:
+ config = config or {}
+ tests = {
+ "curl": curl_test,
+ "wget": wget_test,
+ "http": http_test,
+ "netcat": netcat_test,
+ "tcp": tcp_test,
+ "redis": redis_test,
+ "postgres": postgres_test,
+ "mariadb": mariadb_test,
+ }
+
+ if variant not in tests:
+ raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
+
+ return tests[variant](config)
+
+
+def get_key(config: dict, key: str, default: Any, required: bool):
+ if not config.get(key):
+ if not required:
+ return default
+ raise RenderError(f"Expected [{key}] to be set")
+ return config[key]
+
+
+def curl_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--insecure")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for curl test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "curl --silent --output /dev/null --show-error --fail"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def wget_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--no-check-certificate")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for wget test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "wget --spider --quiet"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def http_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""" # noqa
+
+
+def netcat_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"nc -z -w 1 {host} {port}"
+
+
+def tcp_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"timeout 1 bash -c 'cat < /dev/null > /dev/tcp/{host}/{port}'"
+
+
+def redis_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 6379, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"redis-cli -h {host} -p {port} -a $REDIS_PASSWORD ping | grep -q PONG"
+
+
+def postgres_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 5432, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"pg_isready -h {host} -p {port} -U $POSTGRES_USER -d $POSTGRES_DB"
+
+
+def mariadb_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 3306, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"mariadb-admin --user=root --host={host} --port={port} --password=$MARIADB_ROOT_PASSWORD ping"
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/labels.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/labels.py
new file mode 100644
index 0000000000..f1e667ba00
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/labels.py
@@ -0,0 +1,37 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+
+
+class Labels:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._labels: dict[str, str] = {}
+
+ def add_label(self, key: str, value: str):
+ if not key:
+ raise RenderError("Labels must have a key")
+
+ if key.startswith("com.docker.compose"):
+ raise RenderError(f"Label [{key}] cannot start with [com.docker.compose] as it is reserved")
+
+ if key in self._labels.keys():
+ raise RenderError(f"Label [{key}] already added")
+
+ self._labels[key] = escape_dollar(str(value))
+
+ def has_labels(self) -> bool:
+ return bool(self._labels)
+
+ def render(self) -> dict[str, str]:
+ if not self.has_labels():
+ return {}
+ return {label: value for label, value in sorted(self._labels.items())}
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/notes.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/notes.py
new file mode 100644
index 0000000000..eb8d9f37fc
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/notes.py
@@ -0,0 +1,76 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+class Notes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._app_name: str = ""
+ self._app_train: str = ""
+ self._warnings: list[str] = []
+ self._deprecations: list[str] = []
+ self._header: str = ""
+ self._body: str = ""
+ self._footer: str = ""
+
+ self._auto_set_app_name()
+ self._auto_set_app_train()
+ self._auto_set_header()
+ self._auto_set_footer()
+
+ def _is_enterprise_train(self):
+ if self._app_train == "enterprise":
+ return True
+
+ def _auto_set_app_name(self):
+ app_name = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("title", "")
+ self._app_name = app_name or ""
+
+ def _auto_set_app_train(self):
+ app_train = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("train", "")
+ self._app_train = app_train or ""
+
+ def _auto_set_header(self):
+ self._header = f"# {self._app_name}\n\n"
+
+ def _auto_set_footer(self):
+ url = "https://github.com/truenas/apps"
+ if self._is_enterprise_train():
+ url = "https://ixsystems.atlassian.net"
+ footer = "## Bug Reports and Feature Requests\n\n"
+ footer += "If you find a bug in this app or have an idea for a new feature, please file an issue at\n"
+ footer += f"{url}\n\n"
+ self._footer = footer
+
+ def add_warning(self, warning: str):
+ self._warnings.append(warning)
+
+ def add_deprecation(self, deprecation: str):
+ self._deprecations.append(deprecation)
+
+ def set_body(self, body: str):
+ self._body = body
+
+ def render(self):
+ result = self._header
+
+ if self._warnings:
+ result += "## Warnings\n\n"
+ for warning in self._warnings:
+ result += f"- {warning}\n"
+ result += "\n"
+
+ if self._deprecations:
+ result += "## Deprecations\n\n"
+ for deprecation in self._deprecations:
+ result += f"- {deprecation}\n"
+ result += "\n"
+
+ if self._body:
+ result += self._body.strip() + "\n\n"
+
+ result += self._footer
+
+ return result
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portal.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portal.py
new file mode 100644
index 0000000000..cf47163439
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portal.py
@@ -0,0 +1,22 @@
+try:
+ from .validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+except ImportError:
+ from validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+
+
+class Portal:
+ def __init__(self, name: str, config: dict):
+ self._name = name
+ self._scheme = valid_portal_scheme_or_raise(config.get("scheme", "http"))
+ self._host = config.get("host", "0.0.0.0") or "0.0.0.0"
+ self._port = valid_port_or_raise(config.get("port", 0))
+ self._path = valid_http_path_or_raise(config.get("path", "/"))
+
+ def render(self):
+ return {
+ "name": self._name,
+ "scheme": self._scheme,
+ "host": self._host,
+ "port": self._port,
+ "path": self._path,
+ }
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portals.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portals.py
new file mode 100644
index 0000000000..e106d231e6
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/portals.py
@@ -0,0 +1,28 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .portal import Portal
+except ImportError:
+ from error import RenderError
+ from portal import Portal
+
+
+class Portals:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._portals: set[Portal] = set()
+
+ def add_portal(self, config: dict):
+ name = config.get("name", "Web UI")
+
+ if name in [p._name for p in self._portals]:
+ raise RenderError(f"Portal [{name}] already added")
+
+ self._portals.add(Portal(name, config))
+
+ def render(self):
+ return [p.render() for _, p in sorted([(p._name, p) for p in self._portals])]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/ports.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/ports.py
new file mode 100644
index 0000000000..e571232ac9
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/ports.py
@@ -0,0 +1,153 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+except ImportError:
+ from error import RenderError
+ from validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+
+
+class Ports:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: dict[str, dict] = {}
+
+ def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str:
+ return f"{host_port}_{host_ip}_{proto}_{ip_family}"
+
+ def _is_wildcard_ip(self, ip: str) -> bool:
+ return ip in ["0.0.0.0", "::"]
+
+ def _get_opposite_wildcard(self, ip: str) -> str:
+ return "0.0.0.0" if ip == "::" else "::"
+
+ def _get_sort_key(self, p: dict) -> str:
+ return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}"
+
+ def _is_ports_same(self, port1: dict, port2: dict) -> bool:
+ return (
+ port1["published"] == port2["published"]
+ and port1["target"] == port2["target"]
+ and port1["protocol"] == port2["protocol"]
+ and port1.get("host_ip", "_") == port2.get("host_ip", "_")
+ )
+
+ def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool:
+ comparison_port = port_config.copy()
+ comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"])
+ for p in wildcard_ports.values():
+ if self._is_ports_same(comparison_port, p):
+ return True
+ return False
+
+ def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None:
+ host_port = port_config["published"]
+ host_ip = port_config["host_ip"]
+ proto = port_config["protocol"]
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip_family)
+
+ if key in self._ports.keys():
+ raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]")
+
+ wildcard_ip = "0.0.0.0" if ip_family == 4 else "::"
+ if host_ip != wildcard_ip:
+ # Check if there is a port with same details but with wildcard IP of the same family
+ wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family)
+ if wildcard_key in self._ports.keys():
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{wildcard_ip}]"
+ )
+ else:
+ # We are adding a port with wildcard IP
+ # Check if there is a port with same details but with specific IP of the same family
+ for p in self._ports.values():
+ # Skip if the port is not for the same family
+ if ip_family != ipaddress.ip_address(p["host_ip"]).version:
+ continue
+
+ # Make a copy of the port config
+ search_port = p.copy()
+ # Replace the host IP with wildcard IP
+ search_port["host_ip"] = wildcard_ip
+ # If the ports match, means that a port for specific IP is already added
+ # and we are trying to add it again with wildcard IP. Raise an error
+ if self._is_ports_same(search_port, port_config):
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{p['host_ip']}]"
+ )
+
+ def add_port(self, host_port: int, container_port: int, config: dict | None = None):
+ config = config or {}
+ host_port = valid_port_or_raise(host_port)
+ container_port = valid_port_or_raise(container_port)
+ proto = valid_port_protocol_or_raise(config.get("protocol", "tcp"))
+ mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+
+ # TODO: Once all apps stop using this function directly, (ie using the container.add_port function)
+ # Remove this, and let container.add_port call this for each host_ip
+ host_ip = config.get("host_ip", None)
+ if host_ip is None:
+ self.add_port(host_port, container_port, config | {"host_ip": "0.0.0.0"})
+ self.add_port(host_port, container_port, config | {"host_ip": "::"})
+ return
+
+ host_ip = valid_ip_or_raise(config.get("host_ip", None))
+ ip = ipaddress.ip_address(host_ip)
+
+ port_config = {
+ "published": host_port,
+ "target": container_port,
+ "protocol": proto,
+ "mode": mode,
+ "host_ip": host_ip,
+ }
+ self._check_port_conflicts(port_config, ip.version)
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip.version)
+ self._ports[key] = port_config
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ specific_ports = []
+ wildcard_ports = {}
+
+ for port_config in self._ports.values():
+ if self._is_wildcard_ip(port_config["host_ip"]):
+ wildcard_ports[id(port_config)] = port_config.copy()
+ else:
+ specific_ports.append(port_config.copy())
+
+ processed_ports = specific_ports.copy()
+ for wild_port in wildcard_ports.values():
+ processed_port = wild_port.copy()
+
+ # Check if there's a matching wildcard port for the opposite IP family
+ has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports)
+
+ if has_opposite_family:
+ processed_port.pop("host_ip")
+
+ if processed_port not in processed_ports:
+ processed_ports.append(processed_port)
+
+ return sorted(processed_ports, key=self._get_sort_key)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/render.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/render.py
new file mode 100644
index 0000000000..9d8fcc28d5
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/render.py
@@ -0,0 +1,89 @@
+import copy
+
+try:
+ from .configs import Configs
+ from .container import Container
+ from .deps import Deps
+ from .error import RenderError
+ from .functions import Functions
+ from .notes import Notes
+ from .portals import Portals
+ from .volumes import Volumes
+except ImportError:
+ from configs import Configs
+ from container import Container
+ from deps import Deps
+ from error import RenderError
+ from functions import Functions
+ from notes import Notes
+ from portals import Portals
+ from volumes import Volumes
+
+
+class Render(object):
+ def __init__(self, values):
+ self._containers: dict[str, Container] = {}
+ self.values = values
+ self._add_images_internal_use()
+ # Make a copy after we inject the images
+ self._original_values: dict = copy.deepcopy(self.values)
+
+ self.deps: Deps = Deps(self)
+
+ self.configs = Configs(render_instance=self)
+ self.funcs = Functions(render_instance=self).func_map()
+ self.portals: Portals = Portals(render_instance=self)
+ self.notes: Notes = Notes(render_instance=self)
+ self.volumes = Volumes(render_instance=self)
+
+ def _add_images_internal_use(self):
+ if not self.values.get("images"):
+ self.values["images"] = {}
+
+ if "python_permissions_image" not in self.values["images"]:
+ self.values["images"]["python_permissions_image"] = {"repository": "python", "tag": "3.13.0-slim-bookworm"}
+
+ def container_names(self):
+ return list(self._containers.keys())
+
+ def add_container(self, name: str, image: str):
+ name = name.strip()
+ if not name:
+ raise RenderError("Container name cannot be empty")
+ container = Container(self, name, image)
+ if name in self._containers:
+ raise RenderError(f"Container {name} already exists.")
+ self._containers[name] = container
+ return container
+
+ def render(self):
+ if self.values != self._original_values:
+ raise RenderError("Values have been modified since the renderer was created.")
+
+ if not self._containers:
+ raise RenderError("No containers added.")
+
+ result: dict = {
+ "x-notes": self.notes.render(),
+ "x-portals": self.portals.render(),
+ "services": {c._name: c.render() for c in self._containers.values()},
+ }
+
+ # Make sure that after services are rendered
+ # there are no labels that target a non-existent container
+ # This is to prevent typos
+ for label in self.values.get("labels", []):
+ for c in label.get("containers", []):
+ if c not in self.container_names():
+ raise RenderError(f"Label [{label['key']}] references container [{c}] which does not exist")
+
+ if self.volumes.has_volumes():
+ result["volumes"] = self.volumes.render()
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ # if self.networks:
+ # result["networks"] = {...}
+
+ return result
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/resources.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/resources.py
new file mode 100644
index 0000000000..733f43bb6f
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/resources.py
@@ -0,0 +1,115 @@
+import re
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+DEFAULT_CPUS = 2.0
+DEFAULT_MEMORY = 4096
+
+
+class Resources:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._limits: dict = {}
+ self._reservations: dict = {}
+ self._nvidia_ids: set[str] = set()
+ self._auto_add_cpu_from_values()
+ self._auto_add_memory_from_values()
+ self._auto_add_gpus_from_values()
+
+ def _set_cpu(self, cpus: Any):
+ c = str(cpus)
+ if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", c):
+ raise RenderError(f"Expected cpus to be a number or a float (minimum 1.0), got [{cpus}]")
+ self._limits.update({"cpus": c})
+
+ def _set_memory(self, memory: Any):
+ m = str(memory)
+ if not re.match(r"^[1-9][0-9]*$", m):
+ raise RenderError(f"Expected memory to be a number, got [{memory}]")
+ self._limits.update({"memory": f"{m}M"})
+
+ def _auto_add_cpu_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_cpu(resources.get("limits", {}).get("cpus", DEFAULT_CPUS))
+
+ def _auto_add_memory_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_memory(resources.get("limits", {}).get("memory", DEFAULT_MEMORY))
+
+ def _auto_add_gpus_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ gpus = resources.get("gpus", {}).get("nvidia_gpu_selection", {})
+ if not gpus:
+ return
+
+ for pci, gpu in gpus.items():
+ if gpu.get("use_gpu", False):
+ if not gpu.get("uuid"):
+ raise RenderError(f"Expected [uuid] to be set for GPU in slot [{pci}] in [nvidia_gpu_selection]")
+ self._nvidia_ids.add(gpu["uuid"])
+
+ if self._nvidia_ids:
+ if not self._reservations:
+ self._reservations["devices"] = []
+ self._reservations["devices"].append(
+ {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": sorted(self._nvidia_ids),
+ }
+ )
+
+ # This is only used on ix-app that we allow
+ # disabling cpus and memory. GPUs are only added
+ # if the user has requested them.
+ def remove_cpus_and_memory(self):
+ self._limits.pop("cpus", None)
+ self._limits.pop("memory", None)
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._reservations.pop("devices", None)
+
+ def set_profile(self, profile: str):
+ cpu, memory = profile_mapping(profile)
+ self._set_cpu(cpu)
+ self._set_memory(memory)
+
+ def has_resources(self):
+ return len(self._limits) > 0 or len(self._reservations) > 0
+
+ def has_gpus(self):
+ gpu_devices = [d for d in self._reservations.get("devices", []) if "gpu" in d["capabilities"]]
+ return len(gpu_devices) > 0
+
+ def render(self):
+ result = {}
+ if self._limits:
+ result["limits"] = self._limits
+ if self._reservations:
+ result["reservations"] = self._reservations
+
+ return result
+
+
+def profile_mapping(profile: str):
+ profiles = {
+ "low": (1, 512),
+ "medium": (2, 1024),
+ }
+
+ if profile not in profiles:
+ raise RenderError(
+ f"Resource profile [{profile}] is not valid. Valid options are: [{', '.join(profiles.keys())}]"
+ )
+
+ return profiles[profile]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/restart.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/restart.py
new file mode 100644
index 0000000000..2f6281af48
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/restart.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .validations import valid_restart_policy_or_raise
+except ImportError:
+ from validations import valid_restart_policy_or_raise
+
+
+class RestartPolicy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._policy: str = "unless-stopped"
+ self._maximum_retry_count: int = 0
+
+ def set_policy(self, policy: str, maximum_retry_count: int = 0):
+ self._policy = valid_restart_policy_or_raise(policy, maximum_retry_count)
+ self._maximum_retry_count = maximum_retry_count
+
+ def render(self):
+ if self._policy == "on-failure" and self._maximum_retry_count > 0:
+ return f"{self._policy}:{self._maximum_retry_count}"
+ return self._policy
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/storage.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/storage.py
new file mode 100644
index 0000000000..f1650259b3
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/storage.py
@@ -0,0 +1,116 @@
+from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise
+ from .volume_mount import VolumeMount
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise
+ from volume_mount import VolumeMount
+
+
+class IxStorageTmpfsConfig(TypedDict):
+ size: NotRequired[int]
+ mode: NotRequired[str]
+
+
+class AclConfig(TypedDict, total=False):
+ path: str
+
+
+class IxStorageHostPathConfig(TypedDict):
+ path: NotRequired[str] # Either this or acl.path must be set
+ acl_enable: NotRequired[bool]
+ acl: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageIxVolumeConfig(TypedDict):
+ dataset_name: str
+ acl_enable: NotRequired[bool]
+ acl_entries: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageVolumeConfig(TypedDict):
+ volume_name: NotRequired[str]
+ nocopy: NotRequired[bool]
+ auto_permissions: NotRequired[bool]
+
+
+class IxStorageNfsConfig(TypedDict):
+ server: str
+ path: str
+ options: NotRequired[list[str]]
+
+
+class IxStorageCifsConfig(TypedDict):
+ server: str
+ path: str
+ username: str
+ password: str
+ domain: NotRequired[str]
+ options: NotRequired[list[str]]
+
+
+IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
+IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
+IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
+
+
+class IxStorage(TypedDict):
+ type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
+ read_only: NotRequired[bool]
+
+ ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
+ host_path_config: NotRequired[IxStorageHostPathConfig]
+ tmpfs_config: NotRequired[IxStorageTmpfsConfig]
+ volume_config: NotRequired[IxStorageVolumeConfig]
+ nfs_config: NotRequired[IxStorageNfsConfig]
+ cifs_config: NotRequired[IxStorageCifsConfig]
+
+
+class Storage:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volume_mounts: set[VolumeMount] = set()
+
+ def add(self, mount_path: str, config: "IxStorage"):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ if mount_path in [m.mount_path for m in self._volume_mounts]:
+ raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+
+ volume_mount = VolumeMount(self._render_instance, mount_path, config)
+ self._volume_mounts.add(volume_mount)
+
+ def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def _add_udev(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/run/udev", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def has_mounts(self) -> bool:
+ return bool(self._volume_mounts)
+
+ def render(self):
+ return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/sysctls.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/sysctls.py
new file mode 100644
index 0000000000..e6b8469f3b
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/sysctls.py
@@ -0,0 +1,38 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from container import Container
+
+try:
+ from .error import RenderError
+ from .validations import valid_sysctl_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_sysctl_or_raise
+
+
+class Sysctls:
+ def __init__(self, render_instance: "Render", container_instance: "Container"):
+ self._render_instance = render_instance
+ self._container_instance = container_instance
+ self._sysctls: dict = {}
+
+ def add(self, key: str, value):
+ key = key.strip()
+ if not key:
+ raise RenderError("Sysctls key cannot be empty")
+ if value is None:
+ raise RenderError(f"Sysctl [{key}] requires a value")
+ if key in self._sysctls:
+ raise RenderError(f"Sysctl [{key}] already added")
+ self._sysctls[key] = str(value)
+
+ def has_sysctls(self):
+ return bool(self._sysctls)
+
+ def render(self):
+ if not self.has_sysctls():
+ return {}
+ host_net = self._container_instance._network_mode == "host"
+ return {valid_sysctl_or_raise(k, host_net): v for k, v in self._sysctls.items()}
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/__init__.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/__init__.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/__init__.py
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/__init__.py
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_build_image.py
new file mode 100644
index 0000000000..f30c1210ed
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_build_image.py
@@ -0,0 +1,49 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_build_image_with_from(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.build_image(["FROM test_image"])
+
+
+def test_build_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.build_image(
+ [
+ "RUN echo hello",
+ None,
+ "",
+ "RUN echo world",
+ ]
+ )
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["image"]
+ == "ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"
+ )
+ assert output["services"]["test_container"]["build"] == {
+ "tags": ["ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"],
+ "dockerfile_inline": """FROM nginx:latest
+RUN echo hello
+RUN echo world
+""",
+ }
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_configs.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_configs.py
new file mode 100644
index 0000000000..9049e473ea
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_configs.py
@@ -0,0 +1,63 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_duplicate_config_with_different_data(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data2", "/some/path")
+
+
+def test_add_config_with_empty_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data", "")
+
+
+def test_add_duplicate_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config2", "test_data2", "/some/path")
+
+
+def test_add_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "$test_data", "/some/path")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "$$test_data"
+ assert output["services"]["test_container"]["configs"] == [{"source": "test_config", "target": "/some/path"}]
+
+
+def test_add_config_with_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path", "0777")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "test_data"
+ assert output["services"]["test_container"]["configs"] == [
+ {"source": "test_config", "target": "/some/path", "mode": 511}
+ ]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_container.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_container.py
new file mode 100644
index 0000000000..1980dcd5df
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_container.py
@@ -0,0 +1,425 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_empty_container_name(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container(" ", "test_image")
+
+
+def test_resolve_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["image"] == "nginx:latest"
+
+
+def test_missing_repo(mock_values):
+ mock_values["images"]["test_image"]["repository"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_missing_tag(mock_values):
+ mock_values["images"]["test_image"]["tag"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_non_existing_image(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "non_existing_image")
+
+
+def test_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_pull_policy("always")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["pull_policy"] == "always"
+
+
+def test_invalid_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.set_pull_policy("invalid_policy")
+
+
+def test_clear_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.add_caps(["NET_ADMIN"])
+ c1.clear_caps()
+ c1.healthcheck.disable()
+ output = render.render()
+ assert "cap_drop" not in output["services"]["test_container"]
+ assert "cap_add" not in output["services"]["test_container"]
+
+
+def test_privileged(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_privileged(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["privileged"] is True
+
+
+def test_tty(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_tty(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["tty"] is True
+
+
+def test_init(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_init(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["init"] is True
+
+
+def test_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_read_only(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["read_only"] is True
+
+
+def test_stdin(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_stdin(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stdin_open"] is True
+
+
+def test_hostname(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_hostname("test_hostname")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["hostname"] == "test_hostname"
+
+
+def test_grace_period(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_grace_period(10)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stop_grace_period"] == "10s"
+
+
+def test_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_user(1000, 1000)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["user"] == "1000:1000"
+
+
+def test_invalid_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_user(-100, 1000)
+
+
+def test_add_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ c1.add_group("video")
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
+
+
+def test_add_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ with pytest.raises(Exception):
+ c1.add_group(1000)
+
+
+def test_add_group_as_string(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_group("1000")
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_docker_socket()
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 999]
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {
+ "propagation": "rprivate",
+ "create_host_path": False,
+ },
+ }
+ ]
+
+
+def test_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_shm_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_shm_size_mb(10)
+ output = render.render()
+ assert output["services"]["test_container"]["shm_size"] == "10M"
+
+
+def test_valid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_caps(["ALL", "NET_ADMIN"])
+ output = render.render()
+ assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
+ assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
+
+
+def test_add_duplicate_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
+
+
+def test_invalid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["invalid_cap"])
+
+
+def test_remove_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.remove_security_opt("no-new-privileges")
+ output = render.render()
+ assert "security_opt" not in output["services"]["test_container"]
+
+
+def test_add_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_security_opt("seccomp=unconfined")
+ output = render.render()
+ assert output["services"]["test_container"]["security_opt"] == [
+ "no-new-privileges",
+ "seccomp=unconfined",
+ ]
+
+
+def test_add_duplicate_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_security_opt("no-new-privileges")
+
+
+def test_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_auto_network_mode_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_network_mode_with_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("service:test_container")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "service:test_container"
+
+
+def test_network_mode_with_container_missing(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("service:missing_container")
+
+
+def test_invalid_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("invalid_mode")
+
+
+def test_entrypoint(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
+
+
+def test_command(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_command(["echo", "hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
+
+
+def test_add_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
+ c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
+ c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
+ c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
+ c1.add_port(
+ {"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
+ {"container_port": 9092, "protocol": "udp"},
+ )
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
+ ]
+ assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
+
+
+def test_add_ports_with_invalid_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+ ]
+
+
+def test_set_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c2 = render.add_container("test_container2", "test_image")
+ c2.healthcheck.disable()
+ c1.set_ipc_mode("container:test_container2")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("container:invalid")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_depends.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_depends.py
new file mode 100644
index 0000000000..a1d8373927
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_depends.py
@@ -0,0 +1,54 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_dependency(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ c1.depends.add_dependency("test_container2", "service_started")
+ output = render.render()
+ assert output["services"]["test_container"]["depends_on"]["test_container2"] == {"condition": "service_started"}
+
+
+def test_add_dependency_invalid_condition(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ render.add_container("test_container2", "test_image")
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "invalid_condition")
+
+
+def test_add_dependency_missing_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
+
+
+def test_add_dependency_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ render.add_container("test_container2", "test_image")
+ c1.depends.add_dependency("test_container2", "service_started")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_deps.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_deps.py
new file mode 100644
index 0000000000..a1b7f03a60
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_deps.py
@@ -0,0 +1,477 @@
+import json
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_postgres_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ p = render.deps.postgres(
+ "pg_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_@password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ p.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert (
+ p.get_url("postgres") == "postgres://test_user:test_%40password@pg_container:5432/test_database?sslmode=disable"
+ )
+ assert "devices" not in output["services"]["pg_container"]
+ assert "reservations" not in output["services"]["pg_container"]["deploy"]["resources"]
+ assert output["services"]["pg_container"]["image"] == "postgres:16"
+ assert output["services"]["pg_container"]["user"] == "999:999"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["pg_container"]["healthcheck"] == {
+ "test": "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["pg_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/postgresql/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["pg_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "POSTGRES_USER": "test_user",
+ "POSTGRES_PASSWORD": "test_@password",
+ "POSTGRES_DB": "test_database",
+ "POSTGRES_PORT": "5432",
+ }
+ assert output["services"]["pg_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"},
+ "pg_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert output["services"]["perms_container"]["restart"] == "on-failure:1"
+
+
+def test_add_redis_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test_password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis_with_password_with_spaces(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ r = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test&password@",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ c1.environment.add_env("REDIS_URL", r.get_url("redis"))
+ if perms_container.has_actions():
+ perms_container.activate()
+ r.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["redis_container"]
+ assert "reservations" not in output["services"]["redis_container"]["deploy"]["resources"]
+ assert (
+ output["services"]["test_container"]["environment"]["REDIS_URL"]
+ == "redis://default:test%26password%40@redis_container:6379"
+ )
+ assert output["services"]["redis_container"]["image"] == "redis:latest"
+ assert output["services"]["redis_container"]["user"] == "1001:0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["redis_container"]["healthcheck"] == {
+ "test": "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["redis_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/bitnami/redis/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["redis_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "ALLOW_EMPTY_PASSWORD": "no",
+ "REDIS_PASSWORD": "test&password@",
+ "REDIS_PORT_NUMBER": "6379",
+ }
+ assert output["services"]["redis_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_mariadb_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.mariadb(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_mariadb(mock_values):
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ m = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["mariadb_container"]
+ assert "reservations" not in output["services"]["mariadb_container"]["deploy"]["resources"]
+ assert output["services"]["mariadb_container"]["image"] == "mariadb:latest"
+ assert output["services"]["mariadb_container"]["user"] == "999:999"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["mariadb_container"]["healthcheck"] == {
+ "test": "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["mariadb_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/mysql",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["mariadb_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "MARIADB_USER": "test_user",
+ "MARIADB_PASSWORD": "test_password",
+ "MARIADB_ROOT_PASSWORD": "test_password",
+ "MARIADB_DATABASE": "test_database",
+ "MARIADB_AUTO_UPGRADE": "true",
+ }
+ assert output["services"]["mariadb_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_perms_container(mock_values):
+ mock_values["ix_volumes"] = {
+ "test_dataset1": "/mnt/test/1",
+ "test_dataset2": "/mnt/test/2",
+ "test_dataset3": "/mnt/test/3",
+ }
+ mock_values["images"]["postgres_image"] = {"repository": "postgres", "tag": "17"}
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ # fmt: off
+ volume_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ volume_no_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ host_path_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "auto_permissions": True}}
+ host_path_no_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ host_path_acl_perms = {"type": "host_path", "host_path_config": {"acl": {"path": "/mnt/test"}, "acl_enable": True, "auto_permissions": True}} # noqa
+ ix_volume_no_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset1"}}
+ ix_volume_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset2", "auto_permissions": True}} # noqa
+ ix_volume_acl_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset3", "acl_enable": True, "auto_permissions": True}} # noqa
+ temp_volume = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ # fmt: on
+
+ c1.add_storage("/some/path1", volume_perms)
+ c1.add_storage("/some/path2", volume_no_perms)
+ c1.add_storage("/some/path3", host_path_perms)
+ c1.add_storage("/some/path4", host_path_no_perms)
+ c1.add_storage("/some/path5", host_path_acl_perms)
+ c1.add_storage("/some/path6", ix_volume_no_perms)
+ c1.add_storage("/some/path7", ix_volume_perms)
+ c1.add_storage("/some/path8", ix_volume_acl_perms)
+ c1.add_storage("/some/path9", temp_volume)
+
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data2", volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data3", host_path_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data4", host_path_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data5", host_path_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data6", ix_volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data7", ix_volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data8", ix_volume_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data9", temp_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+ postgres = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ redis = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test_password",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ mariadb = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ postgres.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ redis.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ mariadb.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert output["services"]["test_perms_container"]["network_mode"] == "none"
+ assert output["services"]["test_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+ assert output["configs"]["permissions_run_script"]["content"] != ""
+ # fmt: off
+ content = [
+ {"mount_path": "/mnt/permission/data", "is_temporary": False, "identifier": "data", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data3", "is_temporary": False, "identifier": "data3", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data6", "is_temporary": False, "identifier": "data6", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data7", "is_temporary": False, "identifier": "data7", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data9", "is_temporary": True, "identifier": "data9", "recursive": True, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/postgres_container_postgres_data", "is_temporary": False, "identifier": "postgres_container_postgres_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/redis_container_redis_data", "is_temporary": False, "identifier": "redis_container_redis_data", "recursive": False, "mode": "check", "uid": 1001, "gid": 0, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/mariadb_container_mariadb_data", "is_temporary": False, "identifier": "mariadb_container_mariadb_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ ]
+ # fmt: on
+ assert output["configs"]["permissions_actions_data"]["content"] == json.dumps(content)
+
+
+def test_add_duplicate_perms_action(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ with pytest.raises(Exception):
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+
+def test_add_perm_action_without_auto_perms_enabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": False}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "configs" not in output
+ assert "ix-test_perms_container" not in output["services"]
+ assert "depends_on" not in output["services"]["test_container"]
+
+
+def test_add_unsupported_postgres_version(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "99"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres_with_invalid_tag(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_no_upgrade_container_with_non_postgres_image(mock_values):
+ mock_values["images"]["postgres_image"] = {"repository": "tensorchord/pgvecto-rs", "tag": "pg15-v0.2.0"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert len(output["services"]) == 3 # c1, pg, perms
+ assert output["services"]["postgres_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_postgres_with_upgrade_container(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": 16.6}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ pg = output["services"]["postgres_container"]
+ pgup = output["services"]["postgres_container_upgrade"]
+ assert pg["volumes"] == pgup["volumes"]
+ assert pg["user"] == pgup["user"]
+ assert pgup["environment"]["TARGET_VERSION"] == "16"
+ assert pgup["environment"]["DATA_DIR"] == "/var/lib/postgresql/data"
+ pgup_env = pgup["environment"]
+ pgup_env.pop("TARGET_VERSION")
+ pgup_env.pop("DATA_DIR")
+ assert pg["environment"] == pgup_env
+ assert pg["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"},
+ "postgres_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert pgup["depends_on"] == {"test_perms_container": {"condition": "service_completed_successfully"}}
+ assert pgup["restart"] == "on-failure:1"
+ assert pgup["healthcheck"] == {"disable": True}
+ assert pgup["build"]["dockerfile_inline"] != ""
+ assert pgup["configs"][0]["source"] == "pg_container_upgrade.sh"
+ assert pgup["entrypoint"] == ["/bin/bash", "-c", "/upgrade.sh"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device.py
new file mode 100644
index 0000000000..2e71daa5a0
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device.py
@@ -0,0 +1,150 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
+
+
+def test_devices_without_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("", "/c/dev/sda")
+
+
+def test_devices_without_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "")
+
+
+def test_add_duplicate_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_container_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "c/dev/sda")
+
+
+def test_add_device_with_invalid_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("h/dev/sda", "/c/dev/sda")
+
+
+def test_add_disallowed_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/dri", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_cgroup_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
+
+
+def test_automatically_add_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_automatically_add_gpu_devices_and_kfd(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_remove_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.remove_devices()
+ output = render.render()
+ assert "devices" not in output["services"]["test_container"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_add_usb_bus(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_usb_bus()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
+
+
+def test_add_usb_bus_disallowed(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
+
+
+def test_add_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_add_tun_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_tun_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/net/tun:/dev/net/tun"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
new file mode 100644
index 0000000000..581fe82017
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_device_cgroup_rule(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ c1.add_device_cgroup_rule("b 10:20 rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["device_cgroup_rules"] == [
+ "b 10:20 rwm",
+ "c 13:* rwm",
+ ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 rwd")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_dns.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_dns.py
new file mode 100644
index 0000000000..fe6b21e34f
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_dns.py
@@ -0,0 +1,64 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "opt1", "opt2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_opt"] == ["attempts:3", "opt1", "opt2"]
+
+
+def test_auto_add_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_search"] == ["search1", "search2"]
+
+
+def test_auto_add_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns"] == ["nameserver1", "nameserver2"]
+
+
+def test_add_duplicate_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "attempts:5"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_environment.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_environment.py
new file mode 100644
index 0000000000..d657646582
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_environment.py
@@ -0,0 +1,196 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ mock_values["run_as"] = {"user": "1000", "group": "1000"}
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert len(envs) == 11
+ assert envs["TZ"] == "Etc/UTC"
+ assert envs["PUID"] == "1000"
+ assert envs["UID"] == "1000"
+ assert envs["USER_ID"] == "1000"
+ assert envs["PGID"] == "1000"
+ assert envs["GID"] == "1000"
+ assert envs["GROUP_ID"] == "1000"
+ assert envs["UMASK"] == "002"
+ assert envs["UMASK_SET"] == "002"
+ assert envs["NVIDIA_DRIVER_CAPABILITIES"] == "all"
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "uuid_0,uuid_1"
+
+
+def test_skip_generic_variables(mock_values):
+ mock_values["skip_generic_variables"] = True
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+
+ assert len(envs) == 1
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
+
+
+def test_add_from_all_sources(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_value")
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_value"
+ assert envs["USER_ENV"] == "test_value2"
+ assert envs["TZ"] == "Etc/UTC"
+
+
+def test_user_add_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV2", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["MY_ENV"] == "test_value"
+ assert envs["MY_ENV2"] == "test_value2"
+
+
+def test_user_add_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV", "value": "test_value2"},
+ ]
+ )
+
+
+def test_user_env_without_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "", "value": "test_value"},
+ ]
+ )
+
+
+def test_user_env_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "TZ", "value": "test_value"},
+ ]
+ )
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_user_env_try_to_overwrite_app_dev_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "PORT", "value": "test_value"},
+ ]
+ )
+ c1.environment.add_env("PORT", "test_value2")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_vars_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("TZ", "test_value")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_no_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_env("", "test_value")
+
+
+def test_app_dev_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("PORT", "test_value")
+ with pytest.raises(Exception):
+ c1.environment.add_env("PORT", "test_value2")
+
+
+def test_format_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_$value")
+ c1.environment.add_env("APP_ENV_BOOL", True)
+ c1.environment.add_env("APP_ENV_INT", 10)
+ c1.environment.add_env("APP_ENV_FLOAT", 10.5)
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_$value2"},
+ ]
+ )
+
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_$$value"
+ assert envs["USER_ENV"] == "test_$$value2"
+ assert envs["APP_ENV_BOOL"] == "true"
+ assert envs["APP_ENV_INT"] == "10"
+ assert envs["APP_ENV_FLOAT"] == "10.5"
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_expose.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_expose.py
new file mode 100644
index 0000000000..b8724d7548
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_expose.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ c1.expose.add_port(8081, "udp")
+ c1.expose.add_port(8082, "udp")
+ output = render.render()
+ assert output["services"]["test_container"]["expose"] == ["8081/tcp", "8081/udp", "8082/udp"]
+
+
+def test_add_duplicate_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ with pytest.raises(Exception):
+ c1.expose.add_port(8081)
+
+
+def test_add_expose_ports_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ output = render.render()
+ assert "expose" not in output["services"]["test_container"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_extra_hosts.py
new file mode 100644
index 0000000000..35230be16e
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_extra_hosts.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ c1.add_extra_host("test_host2", "127.0.0.2")
+ c1.add_extra_host("host.docker.internal", "host-gateway")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {
+ "host.docker.internal": "host-gateway",
+ "test_host": "127.0.0.1",
+ "test_host2": "127.0.0.2",
+ }
+
+
+def test_add_duplicate_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "::1")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "invalid_ip")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_formatter.py
new file mode 100644
index 0000000000..843cf65d2e
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_formatter.py
@@ -0,0 +1,13 @@
+from formatter import escape_dollar
+
+
+def test_escape_dollar():
+ cases = [
+ {"input": "test", "expected": "test"},
+ {"input": "$test", "expected": "$$test"},
+ {"input": "$$test", "expected": "$$$$test"},
+ {"input": "$$$test", "expected": "$$$$$$test"},
+ {"input": "$test$", "expected": "$$test$$"},
+ ]
+ for case in cases:
+ assert escape_dollar(case["input"]) == case["expected"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_functions.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_functions.py
new file mode 100644
index 0000000000..0ea3b57d18
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_functions.py
@@ -0,0 +1,88 @@
+import re
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_funcs(mock_values):
+ mock_values["ix_volumes"] = {"test": "/mnt/test123"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ tests = [
+ {"func": "auto_cast", "values": ["1"], "expected": 1},
+ {"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
+ {"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
+ {
+ "func": "bcrypt_hash",
+ "values": ["my_pass"],
+ "expect_regex": r"^\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "camel_case", "values": ["my_user"], "expected": "My_User"},
+ {"func": "copy_dict", "values": [{"a": 1}], "expected": {"a": 1}},
+ {"func": "fail", "values": ["my_message"], "expect_raise": True},
+ {
+ "func": "htpasswd",
+ "values": ["my_user", "my_pass"],
+ "expect_regex": r"^my_user:\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "is_boolean", "values": ["true"], "expected": True},
+ {"func": "is_boolean", "values": ["false"], "expected": True},
+ {"func": "is_number", "values": ["1"], "expected": True},
+ {"func": "is_number", "values": ["1.1"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[a-zA-Z0-9]+$"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[0-9]+$"], "expected": False},
+ {"func": "merge_dicts", "values": [{"a": 1}, {"b": 2}], "expected": {"a": 1, "b": 2}},
+ {"func": "must_match_regex", "values": ["my_user", "^[0-9]$"], "expect_raise": True},
+ {"func": "must_match_regex", "values": ["1", "^[0-9]$"], "expected": "1"},
+ {"func": "secure_string", "values": [10], "expect_regex": r"^[a-zA-Z0-9-_]+$"},
+ {"func": "disallow_chars", "values": ["my_user", ["$", "@"], "my_key"], "expected": "my_user"},
+ {"func": "disallow_chars", "values": ["my_user$", ["$", "@"], "my_key"], "expect_raise": True},
+ {
+ "func": "get_host_path",
+ "values": [{"type": "host_path", "host_path_config": {"path": "/mnt/test"}}],
+ "expected": "/mnt/test",
+ },
+ {
+ "func": "get_host_path",
+ "values": [{"type": "ix_volume", "ix_volume_config": {"dataset_name": "test"}}],
+ "expected": "/mnt/test123",
+ },
+ {"func": "or_default", "values": [None, 1], "expected": 1},
+ {"func": "or_default", "values": [1, None], "expected": 1},
+ {"func": "or_default", "values": [False, 1], "expected": 1},
+ {"func": "or_default", "values": [True, 1], "expected": True},
+ {"func": "temp_config", "values": [""], "expect_raise": True},
+ {
+ "func": "temp_config",
+ "values": ["test"],
+ "expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
+ },
+ ]
+
+ for test in tests:
+ print(test["func"], test)
+ func = render.funcs[test["func"]]
+ if test.get("expect_raise", False):
+ with pytest.raises(Exception):
+ func(*test["values"])
+ elif test.get("expect_regex"):
+ r = func(*test["values"])
+ assert re.match(test["expect_regex"], r) is not None
+ else:
+ r = func(*test["values"])
+ assert r == test["expected"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_healthcheck.py
new file mode 100644
index 0000000000..8fa044290f
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_healthcheck.py
@@ -0,0 +1,195 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_disable_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {"disable": True}
+
+
+def test_use_built_in_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.use_built_in()
+ output = render.render()
+ assert "healthcheck" not in output["services"]["test_container"]
+
+
+def test_set_custom_test(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test("echo $1")
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": "echo $$1",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_custom_test_array(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ c1.healthcheck.set_interval(9)
+ c1.healthcheck.set_timeout(8)
+ c1.healthcheck.set_retries(7)
+ c1.healthcheck.set_start_period(6)
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "9s",
+ "timeout": "8s",
+ "retries": 7,
+ "start_period": "6s",
+ }
+
+
+def test_adding_test_when_disabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.healthcheck.set_custom_test("echo $1")
+
+
+def test_not_adding_test(mock_values):
+ render = Render(mock_values)
+ render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_invalid_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.healthcheck.set_test("http", {"port": 8080, "path": "invalid"})
+
+
+def test_http_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("http", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == """/bin/bash -c 'exec {hc_fd}<>/dev/tcp/127.0.0.1/8080 && echo -e "GET / HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n" >&$${hc_fd} && cat <&$${hc_fd} | grep "HTTP" | grep -q "200"'""" # noqa
+ )
+
+
+def test_curl_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "curl --silent --output /dev/null --show-error --fail http://127.0.0.1:8080/health"
+ )
+
+
+def test_curl_healthcheck_with_headers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "headers": [("X-Test", "$test")]})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == 'curl --silent --output /dev/null --show-error --fail --header "X-Test: $$test" http://127.0.0.1:8080/health'
+ )
+
+
+def test_wget_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "wget --spider --quiet http://127.0.0.1:8080/health"
+ )
+
+
+def test_netcat_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("netcat", {"port": 8080})
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"]["test"] == "nc -z -w 1 127.0.0.1 8080"
+
+
+def test_tcp_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("tcp", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/8080'"
+ )
+
+
+def test_redis_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("redis")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG"
+ )
+
+
+def test_postgres_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("postgres")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB"
+ )
+
+
+def test_mariadb_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("mariadb")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping"
+ )
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_labels.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_labels.py
new file mode 100644
index 0000000000..ffa21eceac
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_labels.py
@@ -0,0 +1,88 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_disallowed_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.labels.add_label("com.docker.compose.service", "test_service")
+
+
+def test_add_duplicate_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label", "test_value")
+ with pytest.raises(Exception):
+ c1.labels.add_label("my.custom.label", "test_value1")
+
+
+def test_add_label_on_non_existing_container(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label1", "test_value1")
+ c1.labels.add_label("my.custom.label2", "test_value2")
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+
+
+def test_auto_add_labels(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ {
+ "key": "my.custom.label2",
+ "value": "test_value2",
+ "containers": ["test_container"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+ assert output["services"]["test_container2"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ }
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_notes.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_notes.py
new file mode 100644
index 0000000000..3bdfe33c74
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_notes.py
@@ -0,0 +1,184 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "ix_context": {
+ "app_metadata": {
+ "name": "test_app",
+ "title": "Test App",
+ "train": "enterprise",
+ }
+ },
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_notes(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_on_non_enterprise_train(mock_values):
+ mock_values["ix_context"]["app_metadata"]["train"] = "community"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://github.com/truenas/apps
+
+"""
+ )
+
+
+def test_notes_with_warnings(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_deprecations(mock_values):
+ render = Render(mock_values)
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_body(mock_values):
+ render = Render(mock_values)
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_all(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_portal.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_portal.py
new file mode 100644
index 0000000000..aebd9425c9
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_portal.py
@@ -0,0 +1,75 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_no_portals(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == []
+
+
+def test_add_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ render.portals.add_portal({"name": "Other Portal", "scheme": "https", "path": "/", "port": 8443})
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == [
+ {"name": "Other Portal", "scheme": "https", "host": "0.0.0.0", "port": 8443, "path": "/"},
+ {"name": "Web UI", "scheme": "http", "host": "0.0.0.0", "port": 8080, "path": "/"},
+ ]
+
+
+def test_add_duplicate_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_duplicate_portal_with_explicit_name(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_scheme(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "invalid_scheme", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_path(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "invalid_path", "port": 8080})
+
+
+def test_add_portal_with_invalid_path_double_slash(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/some//path", "port": 8080})
+
+
+def test_add_portal_with_invalid_port(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": -1})
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_ports.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_ports.py
new file mode 100644
index 0000000000..7b37a03600
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_ports.py
@@ -0,0 +1,209 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+tests = [
+ {
+ "name": "add_ports_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8082, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "add_duplicate_ports_should_fail",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_different_protocol_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_same_port_for_both_wildcard_families_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {
+ "published": 8081,
+ "target": 8080,
+ "protocol": "tcp",
+ "mode": "ingress",
+ "host_ip": "fd00:1234:5678:abcd::10",
+ },
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_with_different_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_protocol_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"protocol": "invalid_protocol"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_mode_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"mode": "invalid_mode"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "invalid_ip"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_host_port_should_fail",
+ "inputs": [
+ {"values": (-1, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_container_port_should_fail",
+ "inputs": [
+ {"values": (8081, -1), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_ports_with_different_host_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::11"}), "expect_error": False},
+ ],
+ # fmt: off
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ # fmt: on
+ },
+]
+
+
+@pytest.mark.parametrize("test", tests)
+def test_ports(test):
+ mock_values = {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ errored = False
+ for input in test["inputs"]:
+ if input["expect_error"]:
+ with pytest.raises(Exception):
+ c1.ports.add_port(*input["values"])
+ errored = True
+ else:
+ c1.ports.add_port(*input["values"])
+
+ errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False
+ if errored:
+ return
+
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == test["expected"]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_render.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_render.py
new file mode 100644
index 0000000000..60dc00679e
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_render.py
@@ -0,0 +1,37 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_values_cannot_be_modified(mock_values):
+ render = Render(mock_values)
+ render.values["test"] = "test"
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_duplicate_containers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_no_containers(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_resources.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_resources.py
new file mode 100644
index 0000000000..cd83d164e5
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_resources.py
@@ -0,0 +1,140 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_automatically_add_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": 1.0}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1.0"
+
+
+def test_invalid_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": 1024}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "1024M"
+
+
+def test_invalid_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_gpus(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_0", "uuid_1"],
+ }
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_gpu_without_uuid(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_remove_cpus_and_memory_with_gpus(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_1", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "limits" not in output["services"]["test_container"]["deploy"]["resources"]
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_1"],
+ }
+
+
+def test_remove_cpus_and_memory(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "deploy" not in output["services"]["test_container"]
+
+
+def test_remove_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_devices()
+ output = render.render()
+ assert "reservations" not in output["services"]["test_container"]["deploy"]["resources"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_set_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.set_profile("low")
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1"
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "512M"
+
+
+def test_set_profile_invalid_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.deploy.resources.set_profile("invalid_profile")
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_restart.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_restart.py
new file mode 100644
index 0000000000..06b2975590
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_restart.py
@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_invalid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("invalid_policy")
+
+
+def test_valid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure")
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure"
+
+
+def test_valid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure", 10)
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure:10"
+
+
+def test_invalid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("on-failure", maximum_retry_count=-1)
+
+
+def test_invalid_restart_policy_with_maximum_retry_count_and_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("always", maximum_retry_count=10)
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_sysctls.py
new file mode 100644
index 0000000000..c9414044ea
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_sysctls.py
@@ -0,0 +1,62 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ c1.sysctls.add("fs.mqueue.msg_max", 100)
+ output = render.render()
+ assert output["services"]["test_container"]["sysctls"] == {"net.ipv4.ip_forward": "1", "fs.mqueue.msg_max": "100"}
+
+
+def test_add_net_sysctl_with_host_network(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_duplicate_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ c1.sysctls.add("net.ipv4.ip_forward", 0)
+
+
+def test_add_empty_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.sysctls.add("", 1)
+
+
+def test_add_sysctl_with_invalid_key(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("invalid.sysctl", 1)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_validations.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_validations.py
new file mode 100644
index 0000000000..f0986ce9a5
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/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/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_volumes.py
new file mode 100644
index 0000000000..9a98956bdc
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/tests/test_volumes.py
@@ -0,0 +1,727 @@
+import pytest
+
+
+from render import Render
+from formatter import get_hashed_name_for_volume
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_volume_invalid_type(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "invalid_type"})
+
+
+def test_add_volume_empty_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("", {"type": "tmpfs"})
+
+
+def test_add_volume_duplicate_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+
+
+def test_add_volume_host_path_invalid_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_host_path_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_with_acl_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_mount(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_acl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
+ }
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test/acl",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "slave"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_create_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_invalid_dataset_name(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_no_ix_volume_config(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_volume_mount(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_volume_mount_with_options(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {
+ "type": "ix_volume",
+ "ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
+ }
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rslave"},
+ }
+ ]
+
+
+def test_cifs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_username(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_password(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_without_cifs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["verbose=true", "verbose=true"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["user=username"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": {"verbose": True},
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": [{"verbose": True}],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_add_cifs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_cifs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["vers=3.0", "verbose=true"],
+ }
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "cifs",
+ "device": "//server/path",
+ "o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_without_nfs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {
+ "type": "nfs",
+ "nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_add_nfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path"}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "nfs",
+ "device": ":/path",
+ "o": "addr=server,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_tmpfs_invalid_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_zero_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_invalid_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs"}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "tmpfs",
+ "target": "/some/path",
+ "read_only": False,
+ }
+ ]
+
+
+def test_temporary_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "source": "test_temp_volume",
+ "type": "volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+
+
+def test_docker_volume_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume_missing_volume_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["volumes"] == {"test_volume": {}}
+
+
+def test_anonymous_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
+ ]
+ assert "volumes" not in output
+
+
+def test_add_udev(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev()
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_udev_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev(read_only=False)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/some/path")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_host_path_with_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_host_path_without_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/mnt", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/mnt",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/validations.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/validations.py
new file mode 100644
index 0000000000..d4aa633006
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/validations.py
@@ -0,0 +1,316 @@
+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_port_bind_mode_or_raise(status: str):
+ valid_statuses = ("published", "exposed", "")
+ if status not in valid_statuses:
+ raise RenderError(f"Invalid port status [{status}]")
+ return status
+
+
+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_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+ valid_modes = ("", "host", "private", "shareable", "none")
+ if ipc_mode in valid_modes:
+ return ipc_mode
+ if ipc_mode.startswith("container:"):
+ if ipc_mode[10:] not in containers:
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+ return ipc_mode
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+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 valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+ parts = dev_grp_rule.split(" ")
+ if len(parts) != 3:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [ : ]"
+ )
+
+ valid_types = ("a", "b", "c")
+ if parts[0] not in valid_types:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+ f" but got [{parts[0]}]"
+ )
+
+ major, minor = parts[1].split(":")
+ for part in (major, minor):
+ if part != "*" and not part.isdigit():
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+ f" or [*] but got [{major}] and [{minor}]"
+ )
+
+ valid_cgroup_perm_or_raise(parts[2])
+
+ return dev_grp_rule
+
+
+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/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount.py
new file mode 100644
index 0000000000..aadd077750
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount.py
@@ -0,0 +1,92 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .formatter import merge_dicts_no_overwrite
+ from .volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from .volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+except ImportError:
+ from error import RenderError
+ from formatter import merge_dicts_no_overwrite
+ from volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+
+
+class VolumeMount:
+ def __init__(self, render_instance: "Render", mount_path: str, config: "IxStorage"):
+ self._render_instance = render_instance
+ self.mount_path: str = mount_path
+
+ storage_type: str = config.get("type", "")
+ if not storage_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match storage_type:
+ case "host_path":
+ spec_type = "bind"
+ mount_config = config.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = HostPathSource(self._render_instance, mount_config).get()
+ case "ix_volume":
+ spec_type = "bind"
+ mount_config = config.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = IxVolumeSource(self._render_instance, mount_config).get()
+ case "tmpfs":
+ spec_type = "tmpfs"
+ mount_config = config.get("tmpfs_config", {})
+ mount_type_specific_definition = TmpfsMountType(self._render_instance, mount_config).render()
+ source = None
+ case "nfs":
+ spec_type = "volume"
+ mount_config = config.get("nfs_config")
+ if mount_config is None:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = NfsSource(self._render_instance, mount_config).get()
+ case "cifs":
+ spec_type = "volume"
+ mount_config = config.get("cifs_config")
+ if mount_config is None:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = CifsSource(self._render_instance, mount_config).get()
+ case "volume":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [volume] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "temporary":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [temporary] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "anonymous":
+ spec_type = "volume"
+ mount_config = config.get("volume_config") or {}
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = None
+ case _:
+ raise RenderError(f"Storage type [{storage_type}] is not supported for volume mounts.")
+
+ common_spec = {"type": spec_type, "target": self.mount_path, "read_only": config.get("read_only", False)}
+ if source is not None:
+ common_spec["source"] = source
+ self._render_instance.volumes.add_volume(source, storage_type, mount_config) # type: ignore
+
+ self.volume_mount_spec = merge_dicts_no_overwrite(common_spec, mount_type_specific_definition)
+
+ def render(self) -> dict:
+ return self.volume_mount_spec
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount_types.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount_types.py
new file mode 100644
index 0000000000..00a0ec3a18
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_mount_types.py
@@ -0,0 +1,72 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageTmpfsConfig, IxStorageVolumeConfig, IxStorageBindLikeConfigs
+
+
+try:
+ from .error import RenderError
+ from .validations import valid_host_path_propagation, valid_octal_mode_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_host_path_propagation, valid_octal_mode_or_raise
+
+
+class TmpfsMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageTmpfsConfig"):
+ self._render_instance = render_instance
+ self.spec = {"tmpfs": {}}
+ size = config.get("size", None)
+ mode = config.get("mode", None)
+
+ if size is not None:
+ if not isinstance(size, int):
+ raise RenderError(f"Expected [size] to be an integer for [tmpfs] type, got [{size}]")
+ if not size > 0:
+ raise RenderError(f"Expected [size] to be greater than 0 for [tmpfs] type, got [{size}]")
+ # Convert Mebibytes to Bytes
+ self.spec["tmpfs"]["size"] = size * 1024 * 1024
+
+ if mode is not None:
+ mode = valid_octal_mode_or_raise(mode)
+ self.spec["tmpfs"]["mode"] = int(mode, 8)
+
+ if not self.spec["tmpfs"]:
+ self.spec.pop("tmpfs")
+
+ def render(self) -> dict:
+ """Render the tmpfs mount specification."""
+ return self.spec
+
+
+class BindMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageBindLikeConfigs"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ propagation = valid_host_path_propagation(config.get("propagation", "rprivate"))
+ create_host_path = config.get("create_host_path", False)
+
+ self.spec: dict = {
+ "bind": {
+ "create_host_path": create_host_path,
+ "propagation": propagation,
+ }
+ }
+
+ def render(self) -> dict:
+ """Render the bind mount specification."""
+ return self.spec
+
+
+class VolumeMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ self.spec: dict = {"volume": {"nocopy": config.get("nocopy", False)}}
+
+ def render(self) -> dict:
+ """Render the volume mount specification."""
+ return self.spec
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_sources.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_sources.py
new file mode 100644
index 0000000000..6aba23df23
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_sources.py
@@ -0,0 +1,110 @@
+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("/")
+ # TODO: Hack for Nextcloud deprecated config. Remove once we remove support for it
+ allow_unsafe_ix_volume = config.get("allow_unsafe_ix_volume", False)
+ self.source = allowed_fs_host_path_or_raise(path, allow_unsafe_ix_volume)
+
+ 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/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_types.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_types.py
new file mode 100644
index 0000000000..4ccea08f83
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volume_types.py
@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageNfsConfig, IxStorageCifsConfig, IxStorageVolumeConfig
+
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_fs_path_or_raise
+
+
+class NfsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageNfsConfig"):
+ self._render_instance = render_instance
+
+ if not config:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type")
+
+ required_keys = ["server", "path"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [nfs] type")
+
+ opts = [f"addr={config['server']}"]
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [nfs_config.options] to be a list for [nfs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["addr"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [nfs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [nfs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [nfs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+
+ opts.sort()
+
+ path = valid_fs_path_or_raise(config["path"].rstrip("/"))
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "nfs",
+ "device": f":{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class CifsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageCifsConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ if not config:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type")
+
+ required_keys = ["server", "path", "username", "password"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [cifs] type")
+
+ opts = [
+ "noperm",
+ f"user={config['username']}",
+ f"password={config['password']}",
+ ]
+
+ domain = config.get("domain")
+ if domain:
+ opts.append(f"domain={domain}")
+
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [cifs_config.options] to be a list for [cifs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["user", "password", "domain", "noperm"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [cifs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [cifs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ for disallowed in disallowed_opts:
+ if key == disallowed:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+ opts.sort()
+
+ server = config["server"].lstrip("/")
+ path = config["path"].strip("/")
+ path = valid_fs_path_or_raise("/" + path).lstrip("/")
+
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "cifs",
+ "device": f"//{server}/{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class DockerVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ def get(self):
+ return self.volume_spec
diff --git a/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volumes.py b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volumes.py
new file mode 100644
index 0000000000..e6925a402f
--- /dev/null
+++ b/trains/enterprise/asigra-ds-system/1.0.27/templates/library/base_v2_1_14/volumes.py
@@ -0,0 +1,66 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+try:
+ from .error import RenderError
+ from .storage import IxStorageVolumeLikeConfigs
+ from .volume_types import NfsVolume, CifsVolume, DockerVolume
+except ImportError:
+ from error import RenderError
+ from storage import IxStorageVolumeLikeConfigs
+ from volume_types import NfsVolume, CifsVolume, DockerVolume
+
+
+class Volumes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volumes: dict[str, Volume] = {}
+
+ def add_volume(
+ self,
+ source: str,
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ # This method can be called many times from the volume mounts
+ # Only add the volume if it is not already added, but dont raise an error
+ if source == "":
+ raise RenderError(f"Volume source [{source}] cannot be empty")
+
+ if source in self._volumes:
+ return
+
+ self._volumes[source] = Volume(self._render_instance, storage_type, config)
+
+ def has_volumes(self) -> bool:
+ return bool(self._volumes)
+
+ def render(self):
+ return {name: v.render() for name, v in sorted(self._volumes.items()) if v.render() is not None}
+
+
+class Volume:
+ def __init__(
+ self,
+ render_instance: "Render",
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ self._render_instance = render_instance
+ self.volume_spec: dict | None = {}
+
+ match storage_type:
+ case "nfs":
+ self.volume_spec = NfsVolume(self._render_instance, config).get() # type: ignore
+ case "cifs":
+ self.volume_spec = CifsVolume(self._render_instance, config).get() # type: ignore
+ case "volume" | "temporary":
+ self.volume_spec = DockerVolume(self._render_instance, config).get() # type: ignore
+ case _:
+ self.volume_spec = None
+
+ def render(self):
+ return self.volume_spec
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/macros/haproxy_config.macro b/trains/enterprise/asigra-ds-system/1.0.27/templates/macros/haproxy_config.macro
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/macros/haproxy_config.macro
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/macros/haproxy_config.macro
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/test_values/basic-values.yaml b/trains/enterprise/asigra-ds-system/1.0.27/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/test_values/basic-values.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/test_values/basic-values.yaml
diff --git a/trains/enterprise/asigra-ds-system/1.0.26/templates/test_values/cluster-values.yaml b/trains/enterprise/asigra-ds-system/1.0.27/templates/test_values/cluster-values.yaml
similarity index 100%
rename from trains/enterprise/asigra-ds-system/1.0.26/templates/test_values/cluster-values.yaml
rename to trains/enterprise/asigra-ds-system/1.0.27/templates/test_values/cluster-values.yaml
diff --git a/trains/enterprise/asigra-ds-system/app_versions.json b/trains/enterprise/asigra-ds-system/app_versions.json
index 66a85ab655..bc8a781c90 100644
--- a/trains/enterprise/asigra-ds-system/app_versions.json
+++ b/trains/enterprise/asigra-ds-system/app_versions.json
@@ -1,13 +1,13 @@
{
- "1.0.26": {
+ "1.0.27": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/enterprise/asigra-ds-system/1.0.26",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/enterprise/asigra-ds-system/1.0.27",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "14.2.0.8_1.0.26",
- "version": "1.0.26",
+ "human_version": "14.2.0.8_1.0.27",
+ "version": "1.0.27",
"app_metadata": {
"app_version": "14.2.0.8",
"capabilities": [
@@ -75,7 +75,7 @@
],
"title": "Asigra DS-System",
"train": "enterprise",
- "version": "1.0.26"
+ "version": "1.0.27"
},
"schema": {
"groups": [
diff --git a/trains/enterprise/ix-remote-assist/app_versions.json b/trains/enterprise/ix-remote-assist/app_versions.json
index 0565e11fa8..b092333f94 100644
--- a/trains/enterprise/ix-remote-assist/app_versions.json
+++ b/trains/enterprise/ix-remote-assist/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/enterprise/ix-remote-assist/1.0.1",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.76.6_1.0.1",
"version": "1.0.1",
diff --git a/trains/enterprise/minio/app_versions.json b/trains/enterprise/minio/app_versions.json
index af08f90d89..aa6519256c 100644
--- a/trains/enterprise/minio/app_versions.json
+++ b/trains/enterprise/minio/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/enterprise/minio/1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "RELEASE.2024-12-18T13-15-44Z_1.2.9",
"version": "1.2.9",
diff --git a/trains/enterprise/syncthing/app_versions.json b/trains/enterprise/syncthing/app_versions.json
index e76f4323be..cc7fab47d7 100644
--- a/trains/enterprise/syncthing/app_versions.json
+++ b/trains/enterprise/syncthing/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/enterprise/syncthing/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.29.2_1.1.9",
"version": "1.1.9",
diff --git a/trains/stable/collabora/1.2.12/README.md b/trains/stable/collabora/1.2.13/README.md
similarity index 100%
rename from trains/stable/collabora/1.2.12/README.md
rename to trains/stable/collabora/1.2.13/README.md
diff --git a/trains/stable/collabora/1.2.12/app.yaml b/trains/stable/collabora/1.2.13/app.yaml
similarity index 97%
rename from trains/stable/collabora/1.2.12/app.yaml
rename to trains/stable/collabora/1.2.13/app.yaml
index 08e18166d7..b9c0c4af2c 100644
--- a/trains/stable/collabora/1.2.12/app.yaml
+++ b/trains/stable/collabora/1.2.13/app.yaml
@@ -1,4 +1,4 @@
-app_version: 24.04.12.1.1
+app_version: 24.04.12.2.1
capabilities:
- description: Collabora and Nginx are able to chown files.
name: CHOWN
@@ -53,4 +53,4 @@ sources:
- https://hub.docker.com/r/collabora/code
title: Collabora
train: stable
-version: 1.2.12
+version: 1.2.13
diff --git a/trains/stable/collabora/1.2.12/ix_values.yaml b/trains/stable/collabora/1.2.13/ix_values.yaml
similarity index 93%
rename from trains/stable/collabora/1.2.12/ix_values.yaml
rename to trains/stable/collabora/1.2.13/ix_values.yaml
index 5abfc2174e..671e1b8b61 100644
--- a/trains/stable/collabora/1.2.12/ix_values.yaml
+++ b/trains/stable/collabora/1.2.13/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: collabora/code
- tag: 24.04.12.1.1
+ tag: 24.04.12.2.1
nginx_image:
repository: nginx
tag: 1.27.3
diff --git a/trains/stable/collabora/1.2.12/migrations/migrate_from_kubernetes b/trains/stable/collabora/1.2.13/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migrate_from_kubernetes
rename to trains/stable/collabora/1.2.13/migrations/migrate_from_kubernetes
diff --git a/trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/__init__.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/__init__.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/library/base_v2_1_14/tests/__init__.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/__init__.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/cpu.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/cpu.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/cpu.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/dns_config.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/dns_config.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/dns_config.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/kubernetes_secrets.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/memory.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/memory.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/memory.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/resources.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/resources.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/resources.py
diff --git a/trains/stable/collabora/1.2.12/migrations/migration_helpers/storage.py b/trains/stable/collabora/1.2.13/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/stable/collabora/1.2.12/migrations/migration_helpers/storage.py
rename to trains/stable/collabora/1.2.13/migrations/migration_helpers/storage.py
diff --git a/trains/stable/collabora/1.2.12/questions.yaml b/trains/stable/collabora/1.2.13/questions.yaml
similarity index 100%
rename from trains/stable/collabora/1.2.12/questions.yaml
rename to trains/stable/collabora/1.2.13/questions.yaml
diff --git a/trains/stable/collabora/1.2.12/templates/docker-compose.yaml b/trains/stable/collabora/1.2.13/templates/docker-compose.yaml
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/docker-compose.yaml
rename to trains/stable/collabora/1.2.13/templates/docker-compose.yaml
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/__init__.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/configs.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/configs.py
new file mode 100644
index 0000000000..b76f4b169c
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/configs.py
@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._configs: dict[str, dict] = {}
+
+ def add(self, name: str, data: str):
+ if not isinstance(data, str):
+ raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+ if name not in self._configs:
+ self._configs[name] = {"name": name, "data": data}
+ return
+
+ if data == self._configs[name]["data"]:
+ return
+
+ raise RenderError(f"Config [{name}] already added with different data")
+
+ def has_configs(self):
+ return bool(self._configs)
+
+ def render(self):
+ return {
+ c["name"]: {"content": escape_dollar(c["data"])}
+ for c in sorted(self._configs.values(), key=lambda c: c["name"])
+ }
+
+
+class ContainerConfigs:
+ def __init__(self, render_instance: "Render", configs: Configs):
+ self._render_instance = render_instance
+ self.top_level_configs: Configs = configs
+ self.container_configs: set[ContainerConfig] = set()
+
+ def add(self, name: str, data: str, target: str, mode: str = ""):
+ self.top_level_configs.add(name, data)
+
+ if target == "":
+ raise RenderError(f"Expected [target] to be set for config [{name}]")
+ if mode != "":
+ mode = valid_octal_mode_or_raise(mode)
+
+ if target in [c.target for c in self.container_configs]:
+ raise RenderError(f"Target [{target}] already used for another config")
+ target = valid_fs_path_or_raise(target)
+ self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+ def has_configs(self):
+ return bool(self.container_configs)
+
+ def render(self):
+ return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+ def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+ self._render_instance = render_instance
+ self.source = source
+ self.target = target
+ self.mode = mode
+
+ def render(self):
+ result: dict[str, str | int] = {
+ "source": self.source,
+ "target": self.target,
+ }
+
+ if self.mode:
+ result["mode"] = int(self.mode, 8)
+
+ return result
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/container.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/container.py
new file mode 100644
index 0000000000..8e889be045
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/container.py
@@ -0,0 +1,418 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .configs import ContainerConfigs
+ from .depends import Depends
+ from .deploy import Deploy
+ from .device_cgroup_rules import DeviceCGroupRules
+ from .devices import Devices
+ from .dns import Dns
+ from .environment import Environment
+ from .error import RenderError
+ from .expose import Expose
+ from .extra_hosts import ExtraHosts
+ from .formatter import escape_dollar, get_image_with_hashed_data
+ from .healthcheck import Healthcheck
+ from .labels import Labels
+ from .ports import Ports
+ from .restart import RestartPolicy
+ from .validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from .storage import Storage
+ from .sysctls import Sysctls
+except ImportError:
+ from configs import ContainerConfigs
+ from depends import Depends
+ from deploy import Deploy
+ from device_cgroup_rules import DeviceCGroupRules
+ from devices import Devices
+ from dns import Dns
+ from environment import Environment
+ from error import RenderError
+ from expose import Expose
+ from extra_hosts import ExtraHosts
+ from formatter import escape_dollar, get_image_with_hashed_data
+ from healthcheck import Healthcheck
+ from labels import Labels
+ from ports import Ports
+ from restart import RestartPolicy
+ from validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from storage import Storage
+ from sysctls import Sysctls
+
+
+class Container:
+ def __init__(self, render_instance: "Render", name: str, image: str):
+ self._render_instance = render_instance
+
+ self._name: str = name
+ self._image: str = self._resolve_image(image)
+ self._build_image: str = ""
+ self._pull_policy: str = ""
+ self._user: str = ""
+ self._tty: bool = False
+ self._stdin_open: bool = False
+ self._init: bool | None = None
+ self._read_only: bool | None = None
+ self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+ self._hostname: str = ""
+ self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
+ self._cap_add: set[str] = set()
+ self._security_opt: set[str] = set(["no-new-privileges"])
+ self._privileged: bool = False
+ self._group_add: set[int | str] = set()
+ self._network_mode: str = ""
+ self._entrypoint: list[str] = []
+ self._command: list[str] = []
+ self._grace_period: int | None = None
+ self._shm_size: int | None = None
+ self._storage: Storage = Storage(self._render_instance)
+ self._ipc_mode: str | None = None
+ self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+ self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+ self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+ self.deploy: Deploy = Deploy(self._render_instance)
+ self.networks: set[str] = set()
+ self.devices: Devices = Devices(self._render_instance)
+ self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+ self.dns: Dns = Dns(self._render_instance)
+ self.depends: Depends = Depends(self._render_instance)
+ self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+ self.labels: Labels = Labels(self._render_instance)
+ self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+ self.ports: Ports = Ports(self._render_instance)
+ self.expose: Expose = Expose(self._render_instance)
+
+ self._auto_set_network_mode()
+ self._auto_add_labels()
+ self._auto_add_groups()
+
+ def _auto_add_groups(self):
+ self.add_group(568)
+
+ def _auto_set_network_mode(self):
+ if self._render_instance.values.get("network", {}).get("host_network", False):
+ self.set_network_mode("host")
+
+ def _auto_add_labels(self):
+ labels = self._render_instance.values.get("labels", [])
+ if not labels:
+ return
+
+ for label in labels:
+ containers = label.get("containers", [])
+ if not containers:
+ raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+ if self._name in containers:
+ self.labels.add_label(label["key"], label["value"])
+
+ def _resolve_image(self, image: str):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(
+ f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+ )
+ repo = images[image].get("repository", "")
+ tag = images[image].get("tag", "")
+
+ if not repo:
+ raise RenderError(f"Repository not found for image [{image}]")
+ if not tag:
+ raise RenderError(f"Tag not found for image [{image}]")
+
+ return f"{repo}:{tag}"
+
+ def build_image(self, content: list[str | None]):
+ dockerfile = f"FROM {self._image}\n"
+ for line in content:
+ if not line:
+ continue
+ if line.startswith("FROM"):
+ # TODO: This will also block multi-stage builds
+ # We can revisit this later if we need it
+ raise RenderError(
+ "FROM cannot be used in build image. Define the base image when creating the container."
+ )
+ dockerfile += line + "\n"
+
+ self._build_image = dockerfile
+ self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+ def set_pull_policy(self, pull_policy: str):
+ self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+ def set_user(self, user: int, group: int):
+ for i in (user, group):
+ if not isinstance(i, int) or i < 0:
+ raise RenderError(f"User/Group [{i}] is not valid")
+ self._user = f"{user}:{group}"
+
+ def add_extra_host(self, host: str, ip: str):
+ self._extra_hosts.add_host(host, ip)
+
+ def add_group(self, group: int | str):
+ if isinstance(group, str):
+ group = str(group).strip()
+ if group.isdigit():
+ raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+ if group in self._group_add:
+ raise RenderError(f"Group [{group}] already added")
+ self._group_add.add(group)
+
+ def get_additional_groups(self) -> list[int | str]:
+ result = []
+ if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+ result.append(44) # video
+ result.append(107) # render
+ return result
+
+ def get_current_groups(self) -> list[str]:
+ result = [str(g) for g in self._group_add]
+ result.extend([str(g) for g in self.get_additional_groups()])
+ return result
+
+ def set_tty(self, enabled: bool = False):
+ self._tty = enabled
+
+ def set_stdin(self, enabled: bool = False):
+ self._stdin_open = enabled
+
+ def set_ipc_mode(self, ipc_mode: str):
+ self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+ def add_device_cgroup_rule(self, dev_grp_rule: str):
+ self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+ def set_init(self, enabled: bool = False):
+ self._init = enabled
+
+ def set_read_only(self, enabled: bool = False):
+ self._read_only = enabled
+
+ def set_hostname(self, hostname: str):
+ self._hostname = hostname
+
+ def set_grace_period(self, grace_period: int):
+ if grace_period < 0:
+ raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+ self._grace_period = grace_period
+
+ def set_privileged(self, enabled: bool = False):
+ self._privileged = enabled
+
+ def clear_caps(self):
+ self._cap_add.clear()
+ self._cap_drop.clear()
+
+ def add_caps(self, caps: list[str]):
+ for c in caps:
+ if c in self._cap_add:
+ raise RenderError(f"Capability [{c}] already added")
+ self._cap_add.add(valid_cap_or_raise(c))
+
+ def add_security_opt(self, opt: str):
+ if opt in self._security_opt:
+ raise RenderError(f"Security Option [{opt}] already added")
+ self._security_opt.add(opt)
+
+ def remove_security_opt(self, opt: str):
+ self._security_opt.remove(opt)
+
+ def set_network_mode(self, mode: str):
+ self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+ def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+ port_config = port_config or {}
+ dev_config = dev_config or {}
+ # Merge port_config and dev_config (dev_config has precedence)
+ config = port_config | dev_config
+
+ bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+ # Skip port if its neither published nor exposed
+ if not bind_mode:
+ return
+
+ # Collect port config
+ host_port = config.get("port_number", 0)
+ container_port = config.get("container_port", 0) or host_port
+ protocol = config.get("protocol", "tcp")
+ host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+ if not isinstance(host_ips, list):
+ raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+ if bind_mode == "published":
+ for host_ip in host_ips:
+ self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip})
+ elif bind_mode == "exposed":
+ self.expose.add_port(container_port, protocol)
+
+ def set_entrypoint(self, entrypoint: list[str]):
+ self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+ def set_command(self, command: list[str]):
+ self._command = [escape_dollar(str(e)) for e in command]
+
+ def add_storage(self, mount_path: str, config: "IxStorage"):
+ self._storage.add(mount_path, config)
+
+ def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+ self.add_group(999)
+ self._storage._add_docker_socket(read_only, mount_path)
+
+ def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+ self._storage._add_udev(read_only, mount_path)
+
+ def add_tun_device(self):
+ self.devices._add_tun_device()
+
+ def add_snd_device(self):
+ self.add_group(29)
+ self.devices._add_snd_device()
+
+ def set_shm_size_mb(self, size: int):
+ self._shm_size = size
+
+ # Easily remove devices from the container
+ # Useful in dependencies like postgres and redis
+ # where there is no need to pass devices to them
+ def remove_devices(self):
+ self.deploy.resources.remove_devices()
+ self.devices.remove_devices()
+
+ @property
+ def storage(self):
+ return self._storage
+
+ def render(self) -> dict[str, Any]:
+ if self._network_mode and self.networks:
+ raise RenderError("Cannot set both [network_mode] and [networks]")
+
+ result = {
+ "image": self._image,
+ "platform": "linux/amd64",
+ "tty": self._tty,
+ "stdin_open": self._stdin_open,
+ "restart": self.restart.render(),
+ }
+
+ if self._pull_policy:
+ result["pull_policy"] = self._pull_policy
+
+ if self.healthcheck.has_healthcheck():
+ result["healthcheck"] = self.healthcheck.render()
+
+ if self._hostname:
+ result["hostname"] = self._hostname
+
+ if self._build_image:
+ result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ if self._ipc_mode is not None:
+ result["ipc"] = self._ipc_mode
+
+ if self._device_cgroup_rules.has_rules():
+ result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+ if self._extra_hosts.has_hosts():
+ result["extra_hosts"] = self._extra_hosts.render()
+
+ if self._init is not None:
+ result["init"] = self._init
+
+ if self._read_only is not None:
+ result["read_only"] = self._read_only
+
+ if self._grace_period is not None:
+ result["stop_grace_period"] = f"{self._grace_period}s"
+
+ if self._user:
+ result["user"] = self._user
+
+ for g in self.get_additional_groups():
+ self.add_group(g)
+
+ if self._group_add:
+ result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+ if self._shm_size is not None:
+ result["shm_size"] = f"{self._shm_size}M"
+
+ if self._privileged is not None:
+ result["privileged"] = self._privileged
+
+ if self._cap_drop:
+ result["cap_drop"] = sorted(self._cap_drop)
+
+ if self._cap_add:
+ result["cap_add"] = sorted(self._cap_add)
+
+ if self._security_opt:
+ result["security_opt"] = sorted(self._security_opt)
+
+ if self._network_mode:
+ result["network_mode"] = self._network_mode
+
+ if self.sysctls.has_sysctls():
+ result["sysctls"] = self.sysctls.render()
+
+ if self._network_mode != "host":
+ if self.ports.has_ports():
+ result["ports"] = self.ports.render()
+
+ if self.expose.has_ports():
+ result["expose"] = self.expose.render()
+
+ if self._entrypoint:
+ result["entrypoint"] = self._entrypoint
+
+ if self._command:
+ result["command"] = self._command
+
+ if self.devices.has_devices():
+ result["devices"] = self.devices.render()
+
+ if self.deploy.has_deploy():
+ result["deploy"] = self.deploy.render()
+
+ if self.environment.has_variables():
+ result["environment"] = self.environment.render()
+
+ if self.labels.has_labels():
+ result["labels"] = self.labels.render()
+
+ if self.dns.has_dns_nameservers():
+ result["dns"] = self.dns.render_dns_nameservers()
+
+ if self.dns.has_dns_searches():
+ result["dns_search"] = self.dns.render_dns_searches()
+
+ if self.dns.has_dns_opts():
+ result["dns_opt"] = self.dns.render_dns_opts()
+
+ if self.depends.has_dependencies():
+ result["depends_on"] = self.depends.render()
+
+ if self._storage.has_mounts():
+ result["volumes"] = self._storage.render()
+
+ return result
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/depends.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/depends.py
new file mode 100644
index 0000000000..4e057cf085
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/depends.py
@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_depend_condition_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dependencies: dict[str, str] = {}
+
+ def add_dependency(self, name: str, condition: str):
+ condition = valid_depend_condition_or_raise(condition)
+ if name in self._dependencies.keys():
+ raise RenderError(f"Dependency [{name}] already added")
+ if name not in self._render_instance.container_names():
+ raise RenderError(
+ f"Dependency [{name}] not found in defined containers. "
+ f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+ )
+ self._dependencies[name] = condition
+
+ def has_dependencies(self):
+ return len(self._dependencies) > 0
+
+ def render(self):
+ return {d: {"condition": c} for d, c in self._dependencies.items()}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deploy.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deploy.py
new file mode 100644
index 0000000000..894dbc643b
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deploy.py
@@ -0,0 +1,24 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .resources import Resources
+except ImportError:
+ from resources import Resources
+
+
+class Deploy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self.resources: Resources = Resources(self._render_instance)
+
+ def has_deploy(self):
+ return self.resources.has_resources()
+
+ def render(self):
+ if self.resources.has_resources():
+ return {"resources": self.resources.render()}
+
+ return {}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps.py
new file mode 100644
index 0000000000..e96849f979
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps.py
@@ -0,0 +1,32 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .deps_postgres import PostgresContainer, PostgresConfig
+ from .deps_redis import RedisContainer, RedisConfig
+ from .deps_mariadb import MariadbContainer, MariadbConfig
+ from .deps_perms import PermsContainer
+except ImportError:
+ from deps_postgres import PostgresContainer, PostgresConfig
+ from deps_redis import RedisContainer, RedisConfig
+ from deps_mariadb import MariadbContainer, MariadbConfig
+ from deps_perms import PermsContainer
+
+
+class Deps:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def perms(self, name: str):
+ return PermsContainer(self._render_instance, name)
+
+ def postgres(self, name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer):
+ return PostgresContainer(self._render_instance, name, image, config, perms_instance)
+
+ def redis(self, name: str, image: str, config: RedisConfig, perms_instance: PermsContainer):
+ return RedisContainer(self._render_instance, name, image, config, perms_instance)
+
+ def mariadb(self, name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer):
+ return MariadbContainer(self._render_instance, name, image, config, perms_instance)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_mariadb.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_mariadb.py
new file mode 100644
index 0000000000..baf863b370
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_mariadb.py
@@ -0,0 +1,65 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class MariadbConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ root_password: NotRequired[str]
+ port: NotRequired[int]
+ auto_upgrade: NotRequired[bool]
+ volume: "IxStorage"
+
+
+class MariadbContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for mariadb")
+
+ port = valid_port_or_raise(config.get("port") or 3306)
+ root_password = config.get("root_password") or config["password"]
+ auto_upgrade = config.get("auto_upgrade", True)
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(999, 999)
+ c.healthcheck.set_test("mariadb")
+ c.remove_devices()
+
+ c.add_storage("/var/lib/mysql", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_mariadb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ c.environment.add_env("MARIADB_USER", config["user"])
+ c.environment.add_env("MARIADB_PASSWORD", config["password"])
+ c.environment.add_env("MARIADB_ROOT_PASSWORD", root_password)
+ c.environment.add_env("MARIADB_DATABASE", config["database"])
+ c.environment.add_env("MARIADB_AUTO_UPGRADE", str(auto_upgrade).lower())
+ c.set_command(["--port", str(port)])
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_perms.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_perms.py
new file mode 100644
index 0000000000..cdc5a3820a
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_perms.py
@@ -0,0 +1,252 @@
+import json
+import pathlib
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class PermsContainer:
+ def __init__(self, render_instance: "Render", name: str):
+ self._render_instance = render_instance
+ self._name = name
+ self.actions: set[str] = set()
+ self.parsed_configs: list[dict] = []
+
+ def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ identifier = self.normalize_identifier_for_path(identifier)
+ if identifier in self.actions:
+ raise RenderError(f"Action with id [{identifier}] already used for another permission action")
+
+ parsed_action = self.parse_action(identifier, volume_config, action_config)
+ if parsed_action:
+ self.parsed_configs.append(parsed_action)
+ self.actions.add(identifier)
+
+ def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ valid_modes = [
+ "always", # Always set permissions, without checking.
+ "check", # Checks if permissions are correct, and set them if not.
+ ]
+ mode = action_config.get("mode", "check")
+ uid = action_config.get("uid", None)
+ gid = action_config.get("gid", None)
+ chmod = action_config.get("chmod", None)
+ recursive = action_config.get("recursive", False)
+ mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
+ is_temporary = False
+
+ vol_type = volume_config.get("type", "")
+ match vol_type:
+ case "temporary":
+ # If it is a temporary volume, we force auto permissions
+ # and set is_temporary to True, so it will be cleaned up
+ is_temporary = True
+ recursive = True
+ case "volume":
+ if not volume_config.get("volume_config", {}).get("auto_permissions", False):
+ return None
+ case "host_path":
+ host_path_config = volume_config.get("host_path_config", {})
+ # Skip when ACL enabled
+ if host_path_config.get("acl_enable", False):
+ return None
+ if not host_path_config.get("auto_permissions", False):
+ return None
+ case "ix_volume":
+ ix_vol_config = volume_config.get("ix_volume_config", {})
+ # Skip when ACL enabled
+ if ix_vol_config.get("acl_enable", False):
+ return None
+ # For ix_volumes, we default to auto_permissions = True
+ if not ix_vol_config.get("auto_permissions", True):
+ return None
+ case _:
+ # Skip for other types
+ return None
+
+ if mode not in valid_modes:
+ raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
+ if not isinstance(uid, int) or not isinstance(gid, int):
+ raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
+ if chmod is not None:
+ chmod = valid_octal_mode_or_raise(chmod)
+
+ mount_path = valid_fs_path_or_raise(mount_path)
+ return {
+ "mount_path": mount_path,
+ "volume_config": volume_config,
+ "action_data": {
+ "mount_path": mount_path,
+ "is_temporary": is_temporary,
+ "identifier": identifier,
+ "recursive": recursive,
+ "mode": mode,
+ "uid": uid,
+ "gid": gid,
+ "chmod": chmod,
+ },
+ }
+
+ def normalize_identifier_for_path(self, identifier: str):
+ return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
+
+ def has_actions(self):
+ return bool(self.actions)
+
+ def activate(self):
+ if len(self.parsed_configs) != len(self.actions):
+ raise RenderError("Number of actions and parsed configs does not match")
+
+ if not self.has_actions():
+ raise RenderError("No actions added. Check if there are actions before activating")
+
+ # Add the container and set it up
+ c = self._render_instance.add_container(self._name, "python_permissions_image")
+ c.set_user(0, 0)
+ c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
+ c.set_network_mode("none")
+
+ # Don't attach any devices
+ c.remove_devices()
+
+ c.deploy.resources.set_profile("medium")
+ c.restart.set_policy("on-failure", maximum_retry_count=1)
+ c.healthcheck.disable()
+
+ c.set_entrypoint(["python3", "/script/run.py"])
+ script = "#!/usr/bin/env python3\n"
+ script += get_script()
+ c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
+
+ actions_data: list[dict] = []
+ for parsed in self.parsed_configs:
+ c.add_storage(parsed["mount_path"], parsed["volume_config"])
+ actions_data.append(parsed["action_data"])
+
+ actions_data_json = json.dumps(actions_data)
+ c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
+
+
+def get_script():
+ return """
+import os
+import json
+import time
+import shutil
+
+with open("/script/actions.json", "r") as f:
+ actions_data = json.load(f)
+
+if not actions_data:
+ # If this script is called, there should be actions data
+ raise ValueError("No actions data found")
+
+def fix_perms(path, chmod, recursive=False):
+ print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
+ os.chmod(path, int(chmod, 8))
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chmod(os.path.join(root, f), int(chmod, 8))
+ print("Permissions after changes:")
+ print_chmod_stat()
+
+def fix_owner(path, uid, gid, recursive=False):
+ print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
+ os.chown(path, uid, gid)
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chown(os.path.join(root, f), uid, gid)
+ print("Ownership after changes:")
+ print_chown_stat()
+
+def print_chown_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
+
+def print_chmod_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
+
+def print_chown_diff(curr_stat, uid, gid):
+ print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
+
+def print_chmod_diff(curr_stat, mode):
+ print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
+
+def perform_action(action):
+ start_time = time.time()
+ print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
+
+ if not os.path.isdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not a directory, skipping...")
+ return
+
+ if action["is_temporary"]:
+ print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
+ for item in os.listdir(action["mount_path"]):
+ item_path = os.path.join(action["mount_path"], item)
+
+ # Exclude the safe directory, where we can use to mount files temporarily
+ if os.path.basename(item_path) == "ix-safe":
+ continue
+ if os.path.isdir(item_path):
+ shutil.rmtree(item_path)
+ else:
+ os.remove(item_path)
+
+ if not action["is_temporary"] and os.listdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not empty, skipping...")
+ return
+
+ print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
+ curr_stat = os.stat(action["mount_path"])
+ print_chown_diff(curr_stat, action["uid"], action["gid"])
+ print_chmod_diff(curr_stat, action["chmod"])
+ print("---")
+
+ if action["mode"] == "always":
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ return
+
+ elif action["mode"] == "check":
+ if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
+ print("Ownership is incorrect. Fixing...")
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ else:
+ print("Ownership is correct. Skipping...")
+
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ if oct(curr_stat.st_mode)[3:] != action["chmod"]:
+ print("Permissions are incorrect. Fixing...")
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ else:
+ print("Permissions are correct. Skipping...")
+
+ print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
+ print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
+ print()
+
+if __name__ == "__main__":
+ start_time = time.time()
+ for action in actions_data:
+ perform_action(action)
+ print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
+"""
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_postgres.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_postgres.py
new file mode 100644
index 0000000000..c40e900d07
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_postgres.py
@@ -0,0 +1,279 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class PostgresConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+MAX_POSTGRES_VERSION = 17
+
+
+class PostgresContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+ self._data_dir = "/var/lib/postgresql/data"
+ self._upgrade_name = f"{self._name}_upgrade"
+ self._upgrade_container = None
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for postgres")
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+
+ c.set_user(999, 999)
+ c.healthcheck.set_test("postgres")
+ c.remove_devices()
+ c.add_storage(self._data_dir, config["volume"])
+
+ common_variables = {
+ "POSTGRES_USER": config["user"],
+ "POSTGRES_PASSWORD": config["password"],
+ "POSTGRES_DB": config["database"],
+ "POSTGRES_PORT": port,
+ }
+
+ for k, v in common_variables.items():
+ c.environment.add_env(k, v)
+
+ perms_instance.add_or_skip_action(
+ f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ repo = self._get_repo(image)
+ # eg we don't want to handle upgrades of pg_vector at the moment
+ if repo == "postgres":
+ target_major_version = self._get_target_version(image)
+ upg = self._render_instance.add_container(self._upgrade_name, image)
+ upg.build_image(get_build_manifest())
+ upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
+ upg.configs.add("pg_container_upgrade.sh", get_upgrade_script(), "/upgrade.sh", "0755")
+ upg.restart.set_policy("on-failure", 1)
+ upg.set_user(999, 999)
+ upg.healthcheck.disable()
+ upg.remove_devices()
+ upg.add_storage(self._data_dir, config["volume"])
+ for k, v in common_variables.items():
+ upg.environment.add_env(k, v)
+
+ upg.environment.add_env("TARGET_VERSION", target_major_version)
+ upg.environment.add_env("DATA_DIR", self._data_dir)
+
+ self._upgrade_container = upg
+
+ c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
+
+ def add_dependency(self, container_name: str, condition: str):
+ self._container.depends.add_dependency(container_name, condition)
+ if self._upgrade_container:
+ self._upgrade_container.depends.add_dependency(container_name, condition)
+
+ def _get_port(self):
+ return self._config.get("port") or 5432
+
+ def _get_repo(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ repo = images[image].get("repository", "")
+ if not repo:
+ raise RenderError("Could not determine repo")
+ return repo
+
+ def _get_target_version(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ tag = images[image].get("tag", "")
+ tag = str(tag) # Account for tags like 16.6
+ target_major_version = tag.split(".")[0]
+
+ try:
+ target_major_version = int(target_major_version)
+ except ValueError:
+ raise RenderError(f"Could not determine target major version from tag [{tag}]")
+
+ if target_major_version > MAX_POSTGRES_VERSION:
+ raise RenderError(f"Postgres version [{target_major_version}] is not supported")
+
+ return target_major_version
+
+ def get_url(self, variant: str):
+ user = urllib.parse.quote_plus(self._config["user"])
+ password = urllib.parse.quote_plus(self._config["password"])
+ creds = f"{user}:{password}"
+ addr = f"{self._name}:{self._get_port()}"
+ db = self._config["database"]
+
+ match variant:
+ case "postgres":
+ return f"postgres://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql":
+ return f"postgresql://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql_no_creds":
+ return f"postgresql://{addr}/{db}?sslmode=disable"
+ case "host_port":
+ return addr
+ case _:
+ raise RenderError(f"Expected [variant] to be one of [postgres, postgresql], got [{variant}]")
+
+
+def get_build_manifest() -> list[str | None]:
+ return [
+ f"RUN apt-get update && apt-get install -y {' '.join(get_upgrade_packages())}",
+ "WORKDIR /tmp",
+ ]
+
+
+def get_upgrade_packages():
+ return [
+ "rsync",
+ "postgresql-13",
+ "postgresql-14",
+ "postgresql-15",
+ "postgresql-16",
+ ]
+
+
+def get_upgrade_script():
+ return """
+#!/bin/bash
+set -euo pipefail
+
+get_bin_path() {
+ local version=$1
+ echo "/usr/lib/postgresql/$version/bin"
+}
+
+log() {
+ echo "[ix-postgres-upgrade] - [$(date +'%Y-%m-%d %H:%M:%S')] - $1"
+}
+
+check_writable() {
+ local path=$1
+ if [ ! -w "$path" ]; then
+ log "$path is not writable"
+ exit 1
+ fi
+}
+
+check_writable "$DATA_DIR"
+
+# Don't do anything if its a fresh install.
+if [ ! -f "$DATA_DIR/PG_VERSION" ]; then
+ log "File $DATA_DIR/PG_VERSION does not exist. Assuming this is a fresh install."
+ exit 0
+fi
+
+# Don't do anything if we're already at the target version.
+OLD_VERSION=$(cat "$DATA_DIR/PG_VERSION")
+log "Current version: $OLD_VERSION"
+log "Target version: $TARGET_VERSION"
+if [ "$OLD_VERSION" -eq "$TARGET_VERSION" ]; then
+ log "Already at target version $TARGET_VERSION"
+ exit 0
+fi
+
+# Fail if we're downgrading.
+if [ "$OLD_VERSION" -gt "$TARGET_VERSION" ]; then
+ log "Cannot downgrade from $OLD_VERSION to $TARGET_VERSION"
+ exit 1
+fi
+
+export OLD_PG_BINARY=$(get_bin_path "$OLD_VERSION")
+if [ ! -f "$OLD_PG_BINARY/pg_upgrade" ]; then
+ log "File $OLD_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_PG_BINARY=$(get_bin_path "$TARGET_VERSION")
+if [ ! -f "$NEW_PG_BINARY/pg_upgrade" ]; then
+ log "File $NEW_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_DATA_DIR="/tmp/new-data-dir"
+if [ -d "$NEW_DATA_DIR" ]; then
+ log "Directory $NEW_DATA_DIR already exists."
+ exit 1
+fi
+
+export PGUSER="$POSTGRES_USER"
+log "Creating new data dir and initializing..."
+PGDATA="$NEW_DATA_DIR" eval "initdb --username=$POSTGRES_USER --pwfile=<(echo $POSTGRES_PASSWORD)"
+
+timestamp=$(date +%Y%m%d%H%M%S)
+backup_name="backup-$timestamp-$OLD_VERSION-$TARGET_VERSION.tar.gz"
+log "Backing up $DATA_DIR to $NEW_DATA_DIR/$backup_name"
+tar -czf "$NEW_DATA_DIR/$backup_name" "$DATA_DIR"
+
+log "Using old pg_upgrade [$OLD_PG_BINARY/pg_upgrade]"
+log "Using new pg_upgrade [$NEW_PG_BINARY/pg_upgrade]"
+log "Checking upgrade compatibility of $OLD_VERSION to $TARGET_VERSION..."
+
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql \
+ --check
+
+log "Compatibility check passed."
+
+log "Upgrading from $OLD_VERSION to $TARGET_VERSION..."
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql
+
+log "Upgrade complete."
+
+log "Copying old pg_hba.conf to new pg_hba.conf"
+# We need to carry this over otherwise
+cp "$DATA_DIR/pg_hba.conf" "$NEW_DATA_DIR/pg_hba.conf"
+
+log "Replacing contents of $DATA_DIR with contents of $NEW_DATA_DIR (including the backup)."
+rsync --archive --delete "$NEW_DATA_DIR/" "$DATA_DIR/"
+
+log "Removing $NEW_DATA_DIR."
+rm -rf "$NEW_DATA_DIR"
+
+log "Done."
+"""
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_redis.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_redis.py
new file mode 100644
index 0000000000..d49ebe7683
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/deps_redis.py
@@ -0,0 +1,71 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise, valid_redis_password_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise, valid_redis_password_or_raise
+
+
+class RedisConfig(TypedDict):
+ password: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+class RedisContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: RedisConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+
+ for key in ("password", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for redis")
+
+ valid_redis_password_or_raise(config["password"])
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(1001, 0)
+ c.healthcheck.set_test("redis")
+ c.remove_devices()
+
+ c.add_storage("/bitnami/redis/data", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_redis_data", config["volume"], {"uid": 1001, "gid": 0, "mode": "check"}
+ )
+
+ c.environment.add_env("ALLOW_EMPTY_PASSWORD", "no")
+ c.environment.add_env("REDIS_PASSWORD", config["password"])
+ c.environment.add_env("REDIS_PORT_NUMBER", port)
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ def _get_port(self):
+ return self._config.get("port") or 6379
+
+ def get_url(self, variant: str):
+ addr = f"{self._name}:{self._get_port()}"
+ password = urllib.parse.quote_plus(self._config["password"])
+
+ match variant:
+ case "redis":
+ return f"redis://default:{password}@{addr}"
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device.py
new file mode 100644
index 0000000000..bfe97097cb
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device.py
@@ -0,0 +1,31 @@
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+
+
+class Device:
+ def __init__(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ hd = valid_fs_path_or_raise(host_device.rstrip("/"))
+ cd = valid_fs_path_or_raise(container_device.rstrip("/"))
+ if not hd or not cd:
+ raise RenderError(
+ "Expected [host_device] and [container_device] to be set. "
+ f"Got host_device [{host_device}] and container_device [{container_device}]"
+ )
+
+ cgroup_perm = valid_cgroup_perm_or_raise(cgroup_perm)
+ if not allow_disallowed:
+ hd = allowed_device_or_raise(hd)
+
+ self.cgroup_perm: str = cgroup_perm
+ self.host_device: str = hd
+ self.container_device: str = cd
+
+ def render(self):
+ result = f"{self.host_device}:{self.container_device}"
+ if self.cgroup_perm:
+ result += f":{self.cgroup_perm}"
+ return result
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device_cgroup_rules.py
new file mode 100644
index 0000000000..dcccfee773
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/device_cgroup_rules.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+ def __init__(self, rule: str):
+ rule = valid_device_cgroup_rule_or_raise(rule)
+ parts = rule.split(" ")
+ major, minor = parts[1].split(":")
+
+ self._type = parts[0]
+ self._major = major
+ self._minor = minor
+ self._permissions = parts[2]
+
+ def get_key(self):
+ return f"{self._type}_{self._major}_{self._minor}"
+
+ def render(self):
+ return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._rules: set[DeviceCGroupRule] = set()
+ self._track_rule_combos: set[str] = set()
+
+ def add_rule(self, rule: str):
+ dev_group_rule = DeviceCGroupRule(rule)
+ if dev_group_rule in self._rules:
+ raise RenderError(f"Device Group Rule [{rule}] already added")
+
+ rule_key = dev_group_rule.get_key()
+ if rule_key in self._track_rule_combos:
+ raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+ self._rules.add(dev_group_rule)
+ self._track_rule_combos.add(rule_key)
+
+ def has_rules(self):
+ return len(self._rules) > 0
+
+ def render(self):
+ return sorted([rule.render() for rule in self._rules])
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/devices.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/devices.py
new file mode 100644
index 0000000000..168e98d032
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/devices.py
@@ -0,0 +1,71 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .device import Device
+except ImportError:
+ from error import RenderError
+ from device import Device
+
+
+class Devices:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._devices: set[Device] = set()
+
+ # Tracks all container device paths to make sure they are not duplicated
+ self._container_device_paths: set[str] = set()
+ # Scan values for devices we should automatically add
+ # for example /dev/dri for gpus
+ self._auto_add_devices_from_values()
+
+ def _auto_add_devices_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+
+ if resources.get("gpus", {}).get("use_all_gpus", False):
+ self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
+ if resources["gpus"].get("kfd_device_exists", False):
+ self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
+
+ def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ # Host device can be mapped to multiple container devices,
+ # so we only make sure container devices are not duplicated
+ if container_device in self._container_device_paths:
+ raise RenderError(f"Device with container path [{container_device}] already added")
+
+ self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
+ self._container_device_paths.add(container_device)
+
+ def add_usb_bus(self):
+ self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
+
+ def _add_snd_device(self):
+ self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
+
+ def _add_tun_device(self):
+ self.add_device("/dev/net/tun", "/dev/net/tun", allow_disallowed=True)
+
+ def has_devices(self):
+ return len(self._devices) > 0
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._devices.clear()
+ self._container_device_paths.clear()
+
+ # Check if there are any gpu devices
+ # Used to determine if we should add groups
+ # like 'video' to the container
+ def has_gpus(self):
+ for d in self._devices:
+ if d.host_device == "/dev/dri":
+ return True
+ return False
+
+ def render(self) -> list[str]:
+ return sorted([d.render() for d in self._devices])
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/dns.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/dns.py
new file mode 100644
index 0000000000..d3ae7b19fa
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/dns.py
@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import allowed_dns_opt_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import allowed_dns_opt_or_raise
+
+
+class Dns:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dns_options: set[str] = set()
+ self._dns_searches: set[str] = set()
+ self._dns_nameservers: set[str] = set()
+
+ self._auto_add_dns_opts_from_values()
+ self._auto_add_dns_searches_from_values()
+ self._auto_add_dns_nameservers_from_values()
+
+ def _get_dns_opt_keys(self):
+ return [self._get_key_from_opt(opt) for opt in self._dns_options]
+
+ def _get_key_from_opt(self, opt):
+ return opt.split(":")[0]
+
+ def _auto_add_dns_opts_from_values(self):
+ values = self._render_instance.values
+ for dns_opt in values.get("network", {}).get("dns_opts", []):
+ self.add_dns_opt(dns_opt)
+
+ def _auto_add_dns_searches_from_values(self):
+ values = self._render_instance.values
+ for dns_search in values.get("network", {}).get("dns_searches", []):
+ self.add_dns_search(dns_search)
+
+ def _auto_add_dns_nameservers_from_values(self):
+ values = self._render_instance.values
+ for dns_nameserver in values.get("network", {}).get("dns_nameservers", []):
+ self.add_dns_nameserver(dns_nameserver)
+
+ def add_dns_search(self, dns_search):
+ if dns_search in self._dns_searches:
+ raise RenderError(f"DNS Search [{dns_search}] already added")
+ self._dns_searches.add(dns_search)
+
+ def add_dns_nameserver(self, dns_nameserver):
+ if dns_nameserver in self._dns_nameservers:
+ raise RenderError(f"DNS Nameserver [{dns_nameserver}] already added")
+ self._dns_nameservers.add(dns_nameserver)
+
+ def add_dns_opt(self, dns_opt):
+ # eg attempts:3
+ key = allowed_dns_opt_or_raise(self._get_key_from_opt(dns_opt))
+ if key in self._get_dns_opt_keys():
+ raise RenderError(f"DNS Option [{key}] already added")
+ self._dns_options.add(dns_opt)
+
+ def has_dns_opts(self):
+ return len(self._dns_options) > 0
+
+ def has_dns_searches(self):
+ return len(self._dns_searches) > 0
+
+ def has_dns_nameservers(self):
+ return len(self._dns_nameservers) > 0
+
+ def render_dns_searches(self):
+ return sorted(self._dns_searches)
+
+ def render_dns_opts(self):
+ return sorted(self._dns_options)
+
+ def render_dns_nameservers(self):
+ return sorted(self._dns_nameservers)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/environment.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/environment.py
new file mode 100644
index 0000000000..056763ea80
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/environment.py
@@ -0,0 +1,112 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .resources import Resources
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from resources import Resources
+
+
+class Environment:
+ def __init__(self, render_instance: "Render", resources: Resources):
+ self._render_instance = render_instance
+ self._resources = resources
+ # Stores variables that user defined
+ self._user_vars: dict[str, Any] = {}
+ # Stores variables that are automatically added (based on values)
+ self._auto_variables: dict[str, Any] = {}
+ # Stores variables that are added by the application developer
+ self._app_dev_variables: dict[str, Any] = {}
+
+ self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
+
+ self._auto_add_variables_from_values()
+
+ def _auto_add_variables_from_values(self):
+ if not self._skip_generic_variables:
+ self._add_generic_variables()
+ self._add_nvidia_variables()
+
+ def _add_generic_variables(self):
+ self._auto_variables["TZ"] = self._render_instance.values.get("TZ", "Etc/UTC")
+ self._auto_variables["UMASK"] = self._render_instance.values.get("UMASK", "002")
+ self._auto_variables["UMASK_SET"] = self._render_instance.values.get("UMASK", "002")
+
+ run_as = self._render_instance.values.get("run_as", {})
+ user = run_as.get("user")
+ group = run_as.get("group")
+ if user:
+ self._auto_variables["PUID"] = user
+ self._auto_variables["UID"] = user
+ self._auto_variables["USER_ID"] = user
+ if group:
+ self._auto_variables["PGID"] = group
+ self._auto_variables["GID"] = group
+ self._auto_variables["GROUP_ID"] = group
+
+ def _add_nvidia_variables(self):
+ if self._resources._nvidia_ids:
+ self._auto_variables["NVIDIA_DRIVER_CAPABILITIES"] = "all"
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(sorted(self._resources._nvidia_ids))
+ else:
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = "void"
+
+ def _format_value(self, v: Any) -> str:
+ value = str(v)
+
+ # str(bool) returns "True" or "False",
+ # but we want "true" or "false"
+ if isinstance(v, bool):
+ value = value.lower()
+ return value
+
+ def add_env(self, name: str, value: Any):
+ if not name:
+ raise RenderError(f"Environment variable name cannot be empty. [{name}]")
+ if name in self._app_dev_variables.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{name}] in application developer environment variables."
+ )
+ self._app_dev_variables[name] = value
+
+ def add_user_envs(self, user_env: list[dict]):
+ for item in user_env:
+ if not item.get("name"):
+ raise RenderError(f"Environment variable name cannot be empty. [{item}]")
+ if item["name"] in self._user_vars.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{item['name']}] in user environment variables."
+ )
+ self._user_vars[item["name"]] = item.get("value")
+
+ def has_variables(self):
+ return len(self._auto_variables) > 0 or len(self._user_vars) > 0 or len(self._app_dev_variables) > 0
+
+ def render(self):
+ result: dict[str, str] = {}
+
+ # Add envs from auto variables
+ result.update({k: self._format_value(v) for k, v in self._auto_variables.items()})
+
+ # Track defined keys for faster lookup
+ defined_keys = set(result.keys())
+
+ # Add envs from application developer (prohibit overwriting auto variables)
+ for k, v in self._app_dev_variables.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined automatically from the library.")
+ result[k] = self._format_value(v)
+ defined_keys.add(k)
+
+ # Add envs from user (prohibit overwriting app developer envs and auto variables)
+ for k, v in self._user_vars.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined from the application developer.")
+ result[k] = self._format_value(v)
+
+ return {k: escape_dollar(v) for k, v in result.items()}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/error.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/error.py
new file mode 100644
index 0000000000..aef48d3b02
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/error.py
@@ -0,0 +1,4 @@
+class RenderError(Exception):
+ """Base class for exceptions in this module."""
+
+ pass
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/expose.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/expose.py
new file mode 100644
index 0000000000..a3ac0aec59
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/expose.py
@@ -0,0 +1,31 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_port_or_raise, valid_port_protocol_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_port_or_raise, valid_port_protocol_or_raise
+
+
+class Expose:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: set[str] = set()
+
+ def add_port(self, port: int, protocol: str = "tcp"):
+ port = valid_port_or_raise(port)
+ protocol = valid_port_protocol_or_raise(protocol)
+ key = f"{port}/{protocol}"
+ if key in self._ports:
+ raise RenderError(f"Exposed port [{port}/{protocol}] already added")
+ self._ports.add(key)
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ return sorted(self._ports)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/extra_hosts.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/extra_hosts.py
new file mode 100644
index 0000000000..eaad3bed26
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/extra_hosts.py
@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+
+class ExtraHosts:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._extra_hosts: dict[str, str] = {}
+
+ def add_host(self, host: str, ip: str):
+ if not ip == "host-gateway":
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+ if host in self._extra_hosts:
+ raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+ self._extra_hosts[host] = ip
+
+ def has_hosts(self):
+ return len(self._extra_hosts) > 0
+
+ def render(self):
+ return {host: ip for host, ip in self._extra_hosts.items()}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/formatter.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/formatter.py
new file mode 100644
index 0000000000..24e882f47a
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/formatter.py
@@ -0,0 +1,26 @@
+import json
+import hashlib
+
+
+def escape_dollar(text: str) -> str:
+ return text.replace("$", "$$")
+
+
+def get_hashed_name_for_volume(prefix: str, config: dict):
+ config_hash = hashlib.sha256(json.dumps(config).encode("utf-8")).hexdigest()
+ return f"{prefix}_{config_hash}"
+
+
+def get_hash_with_prefix(prefix: str, data: str):
+ return f"{prefix}_{hashlib.sha256(data.encode('utf-8')).hexdigest()}"
+
+
+def merge_dicts_no_overwrite(dict1, dict2):
+ overlapping_keys = dict1.keys() & dict2.keys()
+ if overlapping_keys:
+ raise ValueError(f"Merging of dicts failed. Overlapping keys: {overlapping_keys}")
+ return {**dict1, **dict2}
+
+
+def get_image_with_hashed_data(image: str, data: str):
+ return get_hash_with_prefix(f"ix-{image}", data)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/functions.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/functions.py
new file mode 100644
index 0000000000..02c9cd4708
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/functions.py
@@ -0,0 +1,149 @@
+import re
+import copy
+import bcrypt
+import secrets
+from base64 import b64encode
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .volume_sources import HostPathSource, IxVolumeSource
+except ImportError:
+ from error import RenderError
+ from volume_sources import HostPathSource, IxVolumeSource
+
+
+class Functions:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def _bcrypt_hash(self, password):
+ hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+ return hashed
+
+ def _htpasswd(self, username, password):
+ hashed = self._bcrypt_hash(password)
+ return username + ":" + hashed
+
+ def _secure_string(self, length):
+ return secrets.token_urlsafe(length)[:length]
+
+ def _basic_auth(self, username, password):
+ return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
+
+ def _basic_auth_header(self, username, password):
+ return f"Basic {self._basic_auth(username, password)}"
+
+ def _fail(self, message):
+ raise RenderError(message)
+
+ def _camel_case(self, string):
+ return string.title()
+
+ def _auto_cast(self, value):
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return float(value)
+ except ValueError:
+ pass
+
+ if value.lower() in ["true", "false"]:
+ return value.lower() == "true"
+
+ return value
+
+ def _match_regex(self, value, regex):
+ if not re.match(regex, value):
+ return False
+ return True
+
+ def _must_match_regex(self, value, regex):
+ if not self._match_regex(value, regex):
+ raise RenderError(f"Expected [{value}] to match [{regex}]")
+ return value
+
+ def _is_boolean(self, string):
+ return string.lower() in ["true", "false"]
+
+ def _is_number(self, string):
+ try:
+ float(string)
+ return True
+ except ValueError:
+ return False
+
+ def _copy_dict(self, dict):
+ return copy.deepcopy(dict)
+
+ def _merge_dicts(self, *dicts):
+ merged_dict = {}
+ for dictionary in dicts:
+ merged_dict.update(dictionary)
+ return merged_dict
+
+ def _disallow_chars(self, string: str, chars: list[str], key: str):
+ for char in chars:
+ if char in string:
+ raise RenderError(f"Disallowed character [{char}] in [{key}]")
+ return string
+
+ def _or_default(self, value, default):
+ if not value:
+ return default
+ return value
+
+ def _temp_config(self, name):
+ if not name:
+ raise RenderError("Expected [name] to be set when calling [temp_config].")
+ return {"type": "temporary", "volume_config": {"volume_name": name}}
+
+ def _get_host_path(self, storage):
+ source_type = storage.get("type", "")
+ if not source_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match source_type:
+ case "host_path":
+ mount_config = storage.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ host_source = HostPathSource(self._render_instance, mount_config).get()
+ return host_source
+ case "ix_volume":
+ mount_config = storage.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ ix_source = IxVolumeSource(self._render_instance, mount_config).get()
+ return ix_source
+ case _:
+ raise RenderError(f"Storage type [{source_type}] does not support host path.")
+
+ def func_map(self):
+ # TODO: Check what is no longer used and remove
+ return {
+ "auto_cast": self._auto_cast,
+ "basic_auth_header": self._basic_auth_header,
+ "basic_auth": self._basic_auth,
+ "bcrypt_hash": self._bcrypt_hash,
+ "camel_case": self._camel_case,
+ "copy_dict": self._copy_dict,
+ "fail": self._fail,
+ "htpasswd": self._htpasswd,
+ "is_boolean": self._is_boolean,
+ "is_number": self._is_number,
+ "match_regex": self._match_regex,
+ "merge_dicts": self._merge_dicts,
+ "must_match_regex": self._must_match_regex,
+ "secure_string": self._secure_string,
+ "disallow_chars": self._disallow_chars,
+ "get_host_path": self._get_host_path,
+ "or_default": self._or_default,
+ "temp_config": self._temp_config,
+ }
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/healthcheck.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/healthcheck.py
new file mode 100644
index 0000000000..0805329284
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/healthcheck.py
@@ -0,0 +1,203 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_http_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_http_path_or_raise
+
+
+class Healthcheck:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._test: str | list[str] = ""
+ self._interval_sec: int = 10
+ self._timeout_sec: int = 5
+ self._retries: int = 30
+ self._start_period_sec: int = 10
+ self._disabled: bool = False
+ self._use_built_in: bool = False
+
+ def _get_test(self):
+ if isinstance(self._test, str):
+ return escape_dollar(self._test)
+
+ return [escape_dollar(t) for t in self._test]
+
+ def disable(self):
+ self._disabled = True
+
+ def use_built_in(self):
+ self._use_built_in = True
+
+ def set_custom_test(self, test: str | list[str]):
+ if self._disabled:
+ raise RenderError("Cannot set custom test when healthcheck is disabled")
+ self._test = test
+
+ def set_test(self, variant: str, config: dict | None = None):
+ config = config or {}
+ self.set_custom_test(test_mapping(variant, config))
+
+ def set_interval(self, interval: int):
+ self._interval_sec = interval
+
+ def set_timeout(self, timeout: int):
+ self._timeout_sec = timeout
+
+ def set_retries(self, retries: int):
+ self._retries = retries
+
+ def set_start_period(self, start_period: int):
+ self._start_period_sec = start_period
+
+ def has_healthcheck(self):
+ return not self._use_built_in
+
+ def render(self):
+ if self._use_built_in:
+ return RenderError("Should not be called when built in healthcheck is used")
+
+ if self._disabled:
+ return {"disable": True}
+
+ if not self._test:
+ raise RenderError("Healthcheck test is not set")
+
+ return {
+ "test": self._get_test(),
+ "interval": f"{self._interval_sec}s",
+ "timeout": f"{self._timeout_sec}s",
+ "retries": self._retries,
+ "start_period": f"{self._start_period_sec}s",
+ }
+
+
+def test_mapping(variant: str, config: dict | None = None) -> str:
+ config = config or {}
+ tests = {
+ "curl": curl_test,
+ "wget": wget_test,
+ "http": http_test,
+ "netcat": netcat_test,
+ "tcp": tcp_test,
+ "redis": redis_test,
+ "postgres": postgres_test,
+ "mariadb": mariadb_test,
+ }
+
+ if variant not in tests:
+ raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
+
+ return tests[variant](config)
+
+
+def get_key(config: dict, key: str, default: Any, required: bool):
+ if not config.get(key):
+ if not required:
+ return default
+ raise RenderError(f"Expected [{key}] to be set")
+ return config[key]
+
+
+def curl_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--insecure")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for curl test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "curl --silent --output /dev/null --show-error --fail"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def wget_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--no-check-certificate")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for wget test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "wget --spider --quiet"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def http_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""" # noqa
+
+
+def netcat_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"nc -z -w 1 {host} {port}"
+
+
+def tcp_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"timeout 1 bash -c 'cat < /dev/null > /dev/tcp/{host}/{port}'"
+
+
+def redis_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 6379, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"redis-cli -h {host} -p {port} -a $REDIS_PASSWORD ping | grep -q PONG"
+
+
+def postgres_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 5432, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"pg_isready -h {host} -p {port} -U $POSTGRES_USER -d $POSTGRES_DB"
+
+
+def mariadb_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 3306, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"mariadb-admin --user=root --host={host} --port={port} --password=$MARIADB_ROOT_PASSWORD ping"
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/labels.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/labels.py
new file mode 100644
index 0000000000..f1e667ba00
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/labels.py
@@ -0,0 +1,37 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+
+
+class Labels:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._labels: dict[str, str] = {}
+
+ def add_label(self, key: str, value: str):
+ if not key:
+ raise RenderError("Labels must have a key")
+
+ if key.startswith("com.docker.compose"):
+ raise RenderError(f"Label [{key}] cannot start with [com.docker.compose] as it is reserved")
+
+ if key in self._labels.keys():
+ raise RenderError(f"Label [{key}] already added")
+
+ self._labels[key] = escape_dollar(str(value))
+
+ def has_labels(self) -> bool:
+ return bool(self._labels)
+
+ def render(self) -> dict[str, str]:
+ if not self.has_labels():
+ return {}
+ return {label: value for label, value in sorted(self._labels.items())}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/notes.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/notes.py
new file mode 100644
index 0000000000..eb8d9f37fc
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/notes.py
@@ -0,0 +1,76 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+class Notes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._app_name: str = ""
+ self._app_train: str = ""
+ self._warnings: list[str] = []
+ self._deprecations: list[str] = []
+ self._header: str = ""
+ self._body: str = ""
+ self._footer: str = ""
+
+ self._auto_set_app_name()
+ self._auto_set_app_train()
+ self._auto_set_header()
+ self._auto_set_footer()
+
+ def _is_enterprise_train(self):
+ if self._app_train == "enterprise":
+ return True
+
+ def _auto_set_app_name(self):
+ app_name = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("title", "")
+ self._app_name = app_name or ""
+
+ def _auto_set_app_train(self):
+ app_train = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("train", "")
+ self._app_train = app_train or ""
+
+ def _auto_set_header(self):
+ self._header = f"# {self._app_name}\n\n"
+
+ def _auto_set_footer(self):
+ url = "https://github.com/truenas/apps"
+ if self._is_enterprise_train():
+ url = "https://ixsystems.atlassian.net"
+ footer = "## Bug Reports and Feature Requests\n\n"
+ footer += "If you find a bug in this app or have an idea for a new feature, please file an issue at\n"
+ footer += f"{url}\n\n"
+ self._footer = footer
+
+ def add_warning(self, warning: str):
+ self._warnings.append(warning)
+
+ def add_deprecation(self, deprecation: str):
+ self._deprecations.append(deprecation)
+
+ def set_body(self, body: str):
+ self._body = body
+
+ def render(self):
+ result = self._header
+
+ if self._warnings:
+ result += "## Warnings\n\n"
+ for warning in self._warnings:
+ result += f"- {warning}\n"
+ result += "\n"
+
+ if self._deprecations:
+ result += "## Deprecations\n\n"
+ for deprecation in self._deprecations:
+ result += f"- {deprecation}\n"
+ result += "\n"
+
+ if self._body:
+ result += self._body.strip() + "\n\n"
+
+ result += self._footer
+
+ return result
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portal.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portal.py
new file mode 100644
index 0000000000..cf47163439
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portal.py
@@ -0,0 +1,22 @@
+try:
+ from .validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+except ImportError:
+ from validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+
+
+class Portal:
+ def __init__(self, name: str, config: dict):
+ self._name = name
+ self._scheme = valid_portal_scheme_or_raise(config.get("scheme", "http"))
+ self._host = config.get("host", "0.0.0.0") or "0.0.0.0"
+ self._port = valid_port_or_raise(config.get("port", 0))
+ self._path = valid_http_path_or_raise(config.get("path", "/"))
+
+ def render(self):
+ return {
+ "name": self._name,
+ "scheme": self._scheme,
+ "host": self._host,
+ "port": self._port,
+ "path": self._path,
+ }
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portals.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portals.py
new file mode 100644
index 0000000000..e106d231e6
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/portals.py
@@ -0,0 +1,28 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .portal import Portal
+except ImportError:
+ from error import RenderError
+ from portal import Portal
+
+
+class Portals:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._portals: set[Portal] = set()
+
+ def add_portal(self, config: dict):
+ name = config.get("name", "Web UI")
+
+ if name in [p._name for p in self._portals]:
+ raise RenderError(f"Portal [{name}] already added")
+
+ self._portals.add(Portal(name, config))
+
+ def render(self):
+ return [p.render() for _, p in sorted([(p._name, p) for p in self._portals])]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/ports.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/ports.py
new file mode 100644
index 0000000000..e571232ac9
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/ports.py
@@ -0,0 +1,153 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+except ImportError:
+ from error import RenderError
+ from validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+
+
+class Ports:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: dict[str, dict] = {}
+
+ def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str:
+ return f"{host_port}_{host_ip}_{proto}_{ip_family}"
+
+ def _is_wildcard_ip(self, ip: str) -> bool:
+ return ip in ["0.0.0.0", "::"]
+
+ def _get_opposite_wildcard(self, ip: str) -> str:
+ return "0.0.0.0" if ip == "::" else "::"
+
+ def _get_sort_key(self, p: dict) -> str:
+ return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}"
+
+ def _is_ports_same(self, port1: dict, port2: dict) -> bool:
+ return (
+ port1["published"] == port2["published"]
+ and port1["target"] == port2["target"]
+ and port1["protocol"] == port2["protocol"]
+ and port1.get("host_ip", "_") == port2.get("host_ip", "_")
+ )
+
+ def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool:
+ comparison_port = port_config.copy()
+ comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"])
+ for p in wildcard_ports.values():
+ if self._is_ports_same(comparison_port, p):
+ return True
+ return False
+
+ def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None:
+ host_port = port_config["published"]
+ host_ip = port_config["host_ip"]
+ proto = port_config["protocol"]
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip_family)
+
+ if key in self._ports.keys():
+ raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]")
+
+ wildcard_ip = "0.0.0.0" if ip_family == 4 else "::"
+ if host_ip != wildcard_ip:
+ # Check if there is a port with same details but with wildcard IP of the same family
+ wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family)
+ if wildcard_key in self._ports.keys():
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{wildcard_ip}]"
+ )
+ else:
+ # We are adding a port with wildcard IP
+ # Check if there is a port with same details but with specific IP of the same family
+ for p in self._ports.values():
+ # Skip if the port is not for the same family
+ if ip_family != ipaddress.ip_address(p["host_ip"]).version:
+ continue
+
+ # Make a copy of the port config
+ search_port = p.copy()
+ # Replace the host IP with wildcard IP
+ search_port["host_ip"] = wildcard_ip
+ # If the ports match, means that a port for specific IP is already added
+ # and we are trying to add it again with wildcard IP. Raise an error
+ if self._is_ports_same(search_port, port_config):
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{p['host_ip']}]"
+ )
+
+ def add_port(self, host_port: int, container_port: int, config: dict | None = None):
+ config = config or {}
+ host_port = valid_port_or_raise(host_port)
+ container_port = valid_port_or_raise(container_port)
+ proto = valid_port_protocol_or_raise(config.get("protocol", "tcp"))
+ mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+
+ # TODO: Once all apps stop using this function directly, (ie using the container.add_port function)
+ # Remove this, and let container.add_port call this for each host_ip
+ host_ip = config.get("host_ip", None)
+ if host_ip is None:
+ self.add_port(host_port, container_port, config | {"host_ip": "0.0.0.0"})
+ self.add_port(host_port, container_port, config | {"host_ip": "::"})
+ return
+
+ host_ip = valid_ip_or_raise(config.get("host_ip", None))
+ ip = ipaddress.ip_address(host_ip)
+
+ port_config = {
+ "published": host_port,
+ "target": container_port,
+ "protocol": proto,
+ "mode": mode,
+ "host_ip": host_ip,
+ }
+ self._check_port_conflicts(port_config, ip.version)
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip.version)
+ self._ports[key] = port_config
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ specific_ports = []
+ wildcard_ports = {}
+
+ for port_config in self._ports.values():
+ if self._is_wildcard_ip(port_config["host_ip"]):
+ wildcard_ports[id(port_config)] = port_config.copy()
+ else:
+ specific_ports.append(port_config.copy())
+
+ processed_ports = specific_ports.copy()
+ for wild_port in wildcard_ports.values():
+ processed_port = wild_port.copy()
+
+ # Check if there's a matching wildcard port for the opposite IP family
+ has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports)
+
+ if has_opposite_family:
+ processed_port.pop("host_ip")
+
+ if processed_port not in processed_ports:
+ processed_ports.append(processed_port)
+
+ return sorted(processed_ports, key=self._get_sort_key)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/render.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/render.py
new file mode 100644
index 0000000000..9d8fcc28d5
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/render.py
@@ -0,0 +1,89 @@
+import copy
+
+try:
+ from .configs import Configs
+ from .container import Container
+ from .deps import Deps
+ from .error import RenderError
+ from .functions import Functions
+ from .notes import Notes
+ from .portals import Portals
+ from .volumes import Volumes
+except ImportError:
+ from configs import Configs
+ from container import Container
+ from deps import Deps
+ from error import RenderError
+ from functions import Functions
+ from notes import Notes
+ from portals import Portals
+ from volumes import Volumes
+
+
+class Render(object):
+ def __init__(self, values):
+ self._containers: dict[str, Container] = {}
+ self.values = values
+ self._add_images_internal_use()
+ # Make a copy after we inject the images
+ self._original_values: dict = copy.deepcopy(self.values)
+
+ self.deps: Deps = Deps(self)
+
+ self.configs = Configs(render_instance=self)
+ self.funcs = Functions(render_instance=self).func_map()
+ self.portals: Portals = Portals(render_instance=self)
+ self.notes: Notes = Notes(render_instance=self)
+ self.volumes = Volumes(render_instance=self)
+
+ def _add_images_internal_use(self):
+ if not self.values.get("images"):
+ self.values["images"] = {}
+
+ if "python_permissions_image" not in self.values["images"]:
+ self.values["images"]["python_permissions_image"] = {"repository": "python", "tag": "3.13.0-slim-bookworm"}
+
+ def container_names(self):
+ return list(self._containers.keys())
+
+ def add_container(self, name: str, image: str):
+ name = name.strip()
+ if not name:
+ raise RenderError("Container name cannot be empty")
+ container = Container(self, name, image)
+ if name in self._containers:
+ raise RenderError(f"Container {name} already exists.")
+ self._containers[name] = container
+ return container
+
+ def render(self):
+ if self.values != self._original_values:
+ raise RenderError("Values have been modified since the renderer was created.")
+
+ if not self._containers:
+ raise RenderError("No containers added.")
+
+ result: dict = {
+ "x-notes": self.notes.render(),
+ "x-portals": self.portals.render(),
+ "services": {c._name: c.render() for c in self._containers.values()},
+ }
+
+ # Make sure that after services are rendered
+ # there are no labels that target a non-existent container
+ # This is to prevent typos
+ for label in self.values.get("labels", []):
+ for c in label.get("containers", []):
+ if c not in self.container_names():
+ raise RenderError(f"Label [{label['key']}] references container [{c}] which does not exist")
+
+ if self.volumes.has_volumes():
+ result["volumes"] = self.volumes.render()
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ # if self.networks:
+ # result["networks"] = {...}
+
+ return result
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/resources.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/resources.py
new file mode 100644
index 0000000000..733f43bb6f
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/resources.py
@@ -0,0 +1,115 @@
+import re
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+DEFAULT_CPUS = 2.0
+DEFAULT_MEMORY = 4096
+
+
+class Resources:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._limits: dict = {}
+ self._reservations: dict = {}
+ self._nvidia_ids: set[str] = set()
+ self._auto_add_cpu_from_values()
+ self._auto_add_memory_from_values()
+ self._auto_add_gpus_from_values()
+
+ def _set_cpu(self, cpus: Any):
+ c = str(cpus)
+ if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", c):
+ raise RenderError(f"Expected cpus to be a number or a float (minimum 1.0), got [{cpus}]")
+ self._limits.update({"cpus": c})
+
+ def _set_memory(self, memory: Any):
+ m = str(memory)
+ if not re.match(r"^[1-9][0-9]*$", m):
+ raise RenderError(f"Expected memory to be a number, got [{memory}]")
+ self._limits.update({"memory": f"{m}M"})
+
+ def _auto_add_cpu_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_cpu(resources.get("limits", {}).get("cpus", DEFAULT_CPUS))
+
+ def _auto_add_memory_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_memory(resources.get("limits", {}).get("memory", DEFAULT_MEMORY))
+
+ def _auto_add_gpus_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ gpus = resources.get("gpus", {}).get("nvidia_gpu_selection", {})
+ if not gpus:
+ return
+
+ for pci, gpu in gpus.items():
+ if gpu.get("use_gpu", False):
+ if not gpu.get("uuid"):
+ raise RenderError(f"Expected [uuid] to be set for GPU in slot [{pci}] in [nvidia_gpu_selection]")
+ self._nvidia_ids.add(gpu["uuid"])
+
+ if self._nvidia_ids:
+ if not self._reservations:
+ self._reservations["devices"] = []
+ self._reservations["devices"].append(
+ {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": sorted(self._nvidia_ids),
+ }
+ )
+
+ # This is only used on ix-app that we allow
+ # disabling cpus and memory. GPUs are only added
+ # if the user has requested them.
+ def remove_cpus_and_memory(self):
+ self._limits.pop("cpus", None)
+ self._limits.pop("memory", None)
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._reservations.pop("devices", None)
+
+ def set_profile(self, profile: str):
+ cpu, memory = profile_mapping(profile)
+ self._set_cpu(cpu)
+ self._set_memory(memory)
+
+ def has_resources(self):
+ return len(self._limits) > 0 or len(self._reservations) > 0
+
+ def has_gpus(self):
+ gpu_devices = [d for d in self._reservations.get("devices", []) if "gpu" in d["capabilities"]]
+ return len(gpu_devices) > 0
+
+ def render(self):
+ result = {}
+ if self._limits:
+ result["limits"] = self._limits
+ if self._reservations:
+ result["reservations"] = self._reservations
+
+ return result
+
+
+def profile_mapping(profile: str):
+ profiles = {
+ "low": (1, 512),
+ "medium": (2, 1024),
+ }
+
+ if profile not in profiles:
+ raise RenderError(
+ f"Resource profile [{profile}] is not valid. Valid options are: [{', '.join(profiles.keys())}]"
+ )
+
+ return profiles[profile]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/restart.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/restart.py
new file mode 100644
index 0000000000..2f6281af48
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/restart.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .validations import valid_restart_policy_or_raise
+except ImportError:
+ from validations import valid_restart_policy_or_raise
+
+
+class RestartPolicy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._policy: str = "unless-stopped"
+ self._maximum_retry_count: int = 0
+
+ def set_policy(self, policy: str, maximum_retry_count: int = 0):
+ self._policy = valid_restart_policy_or_raise(policy, maximum_retry_count)
+ self._maximum_retry_count = maximum_retry_count
+
+ def render(self):
+ if self._policy == "on-failure" and self._maximum_retry_count > 0:
+ return f"{self._policy}:{self._maximum_retry_count}"
+ return self._policy
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/storage.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/storage.py
new file mode 100644
index 0000000000..f1650259b3
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/storage.py
@@ -0,0 +1,116 @@
+from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise
+ from .volume_mount import VolumeMount
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise
+ from volume_mount import VolumeMount
+
+
+class IxStorageTmpfsConfig(TypedDict):
+ size: NotRequired[int]
+ mode: NotRequired[str]
+
+
+class AclConfig(TypedDict, total=False):
+ path: str
+
+
+class IxStorageHostPathConfig(TypedDict):
+ path: NotRequired[str] # Either this or acl.path must be set
+ acl_enable: NotRequired[bool]
+ acl: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageIxVolumeConfig(TypedDict):
+ dataset_name: str
+ acl_enable: NotRequired[bool]
+ acl_entries: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageVolumeConfig(TypedDict):
+ volume_name: NotRequired[str]
+ nocopy: NotRequired[bool]
+ auto_permissions: NotRequired[bool]
+
+
+class IxStorageNfsConfig(TypedDict):
+ server: str
+ path: str
+ options: NotRequired[list[str]]
+
+
+class IxStorageCifsConfig(TypedDict):
+ server: str
+ path: str
+ username: str
+ password: str
+ domain: NotRequired[str]
+ options: NotRequired[list[str]]
+
+
+IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
+IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
+IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
+
+
+class IxStorage(TypedDict):
+ type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
+ read_only: NotRequired[bool]
+
+ ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
+ host_path_config: NotRequired[IxStorageHostPathConfig]
+ tmpfs_config: NotRequired[IxStorageTmpfsConfig]
+ volume_config: NotRequired[IxStorageVolumeConfig]
+ nfs_config: NotRequired[IxStorageNfsConfig]
+ cifs_config: NotRequired[IxStorageCifsConfig]
+
+
+class Storage:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volume_mounts: set[VolumeMount] = set()
+
+ def add(self, mount_path: str, config: "IxStorage"):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ if mount_path in [m.mount_path for m in self._volume_mounts]:
+ raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+
+ volume_mount = VolumeMount(self._render_instance, mount_path, config)
+ self._volume_mounts.add(volume_mount)
+
+ def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def _add_udev(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/run/udev", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def has_mounts(self) -> bool:
+ return bool(self._volume_mounts)
+
+ def render(self):
+ return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/sysctls.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/sysctls.py
new file mode 100644
index 0000000000..e6b8469f3b
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/sysctls.py
@@ -0,0 +1,38 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from container import Container
+
+try:
+ from .error import RenderError
+ from .validations import valid_sysctl_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_sysctl_or_raise
+
+
+class Sysctls:
+ def __init__(self, render_instance: "Render", container_instance: "Container"):
+ self._render_instance = render_instance
+ self._container_instance = container_instance
+ self._sysctls: dict = {}
+
+ def add(self, key: str, value):
+ key = key.strip()
+ if not key:
+ raise RenderError("Sysctls key cannot be empty")
+ if value is None:
+ raise RenderError(f"Sysctl [{key}] requires a value")
+ if key in self._sysctls:
+ raise RenderError(f"Sysctl [{key}] already added")
+ self._sysctls[key] = str(value)
+
+ def has_sysctls(self):
+ return bool(self._sysctls)
+
+ def render(self):
+ if not self.has_sysctls():
+ return {}
+ host_net = self._container_instance._network_mode == "host"
+ return {valid_sysctl_or_raise(k, host_net): v for k, v in self._sysctls.items()}
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/__init__.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_build_image.py
new file mode 100644
index 0000000000..f30c1210ed
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_build_image.py
@@ -0,0 +1,49 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_build_image_with_from(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.build_image(["FROM test_image"])
+
+
+def test_build_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.build_image(
+ [
+ "RUN echo hello",
+ None,
+ "",
+ "RUN echo world",
+ ]
+ )
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["image"]
+ == "ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"
+ )
+ assert output["services"]["test_container"]["build"] == {
+ "tags": ["ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"],
+ "dockerfile_inline": """FROM nginx:latest
+RUN echo hello
+RUN echo world
+""",
+ }
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_configs.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_configs.py
new file mode 100644
index 0000000000..9049e473ea
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_configs.py
@@ -0,0 +1,63 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_duplicate_config_with_different_data(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data2", "/some/path")
+
+
+def test_add_config_with_empty_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data", "")
+
+
+def test_add_duplicate_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config2", "test_data2", "/some/path")
+
+
+def test_add_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "$test_data", "/some/path")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "$$test_data"
+ assert output["services"]["test_container"]["configs"] == [{"source": "test_config", "target": "/some/path"}]
+
+
+def test_add_config_with_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path", "0777")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "test_data"
+ assert output["services"]["test_container"]["configs"] == [
+ {"source": "test_config", "target": "/some/path", "mode": 511}
+ ]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_container.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_container.py
new file mode 100644
index 0000000000..1980dcd5df
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_container.py
@@ -0,0 +1,425 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_empty_container_name(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container(" ", "test_image")
+
+
+def test_resolve_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["image"] == "nginx:latest"
+
+
+def test_missing_repo(mock_values):
+ mock_values["images"]["test_image"]["repository"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_missing_tag(mock_values):
+ mock_values["images"]["test_image"]["tag"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_non_existing_image(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "non_existing_image")
+
+
+def test_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_pull_policy("always")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["pull_policy"] == "always"
+
+
+def test_invalid_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.set_pull_policy("invalid_policy")
+
+
+def test_clear_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.add_caps(["NET_ADMIN"])
+ c1.clear_caps()
+ c1.healthcheck.disable()
+ output = render.render()
+ assert "cap_drop" not in output["services"]["test_container"]
+ assert "cap_add" not in output["services"]["test_container"]
+
+
+def test_privileged(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_privileged(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["privileged"] is True
+
+
+def test_tty(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_tty(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["tty"] is True
+
+
+def test_init(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_init(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["init"] is True
+
+
+def test_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_read_only(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["read_only"] is True
+
+
+def test_stdin(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_stdin(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stdin_open"] is True
+
+
+def test_hostname(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_hostname("test_hostname")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["hostname"] == "test_hostname"
+
+
+def test_grace_period(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_grace_period(10)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stop_grace_period"] == "10s"
+
+
+def test_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_user(1000, 1000)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["user"] == "1000:1000"
+
+
+def test_invalid_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_user(-100, 1000)
+
+
+def test_add_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ c1.add_group("video")
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
+
+
+def test_add_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ with pytest.raises(Exception):
+ c1.add_group(1000)
+
+
+def test_add_group_as_string(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_group("1000")
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_docker_socket()
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 999]
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {
+ "propagation": "rprivate",
+ "create_host_path": False,
+ },
+ }
+ ]
+
+
+def test_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_shm_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_shm_size_mb(10)
+ output = render.render()
+ assert output["services"]["test_container"]["shm_size"] == "10M"
+
+
+def test_valid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_caps(["ALL", "NET_ADMIN"])
+ output = render.render()
+ assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
+ assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
+
+
+def test_add_duplicate_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
+
+
+def test_invalid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["invalid_cap"])
+
+
+def test_remove_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.remove_security_opt("no-new-privileges")
+ output = render.render()
+ assert "security_opt" not in output["services"]["test_container"]
+
+
+def test_add_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_security_opt("seccomp=unconfined")
+ output = render.render()
+ assert output["services"]["test_container"]["security_opt"] == [
+ "no-new-privileges",
+ "seccomp=unconfined",
+ ]
+
+
+def test_add_duplicate_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_security_opt("no-new-privileges")
+
+
+def test_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_auto_network_mode_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_network_mode_with_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("service:test_container")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "service:test_container"
+
+
+def test_network_mode_with_container_missing(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("service:missing_container")
+
+
+def test_invalid_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("invalid_mode")
+
+
+def test_entrypoint(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
+
+
+def test_command(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_command(["echo", "hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
+
+
+def test_add_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
+ c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
+ c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
+ c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
+ c1.add_port(
+ {"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
+ {"container_port": 9092, "protocol": "udp"},
+ )
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
+ ]
+ assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
+
+
+def test_add_ports_with_invalid_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+ ]
+
+
+def test_set_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c2 = render.add_container("test_container2", "test_image")
+ c2.healthcheck.disable()
+ c1.set_ipc_mode("container:test_container2")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("container:invalid")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_depends.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_depends.py
new file mode 100644
index 0000000000..a1d8373927
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_depends.py
@@ -0,0 +1,54 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_dependency(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ c1.depends.add_dependency("test_container2", "service_started")
+ output = render.render()
+ assert output["services"]["test_container"]["depends_on"]["test_container2"] == {"condition": "service_started"}
+
+
+def test_add_dependency_invalid_condition(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ render.add_container("test_container2", "test_image")
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "invalid_condition")
+
+
+def test_add_dependency_missing_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
+
+
+def test_add_dependency_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ render.add_container("test_container2", "test_image")
+ c1.depends.add_dependency("test_container2", "service_started")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_deps.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_deps.py
new file mode 100644
index 0000000000..a1b7f03a60
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_deps.py
@@ -0,0 +1,477 @@
+import json
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_postgres_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ p = render.deps.postgres(
+ "pg_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_@password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ p.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert (
+ p.get_url("postgres") == "postgres://test_user:test_%40password@pg_container:5432/test_database?sslmode=disable"
+ )
+ assert "devices" not in output["services"]["pg_container"]
+ assert "reservations" not in output["services"]["pg_container"]["deploy"]["resources"]
+ assert output["services"]["pg_container"]["image"] == "postgres:16"
+ assert output["services"]["pg_container"]["user"] == "999:999"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["pg_container"]["healthcheck"] == {
+ "test": "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["pg_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/postgresql/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["pg_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "POSTGRES_USER": "test_user",
+ "POSTGRES_PASSWORD": "test_@password",
+ "POSTGRES_DB": "test_database",
+ "POSTGRES_PORT": "5432",
+ }
+ assert output["services"]["pg_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"},
+ "pg_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert output["services"]["perms_container"]["restart"] == "on-failure:1"
+
+
+def test_add_redis_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test_password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis_with_password_with_spaces(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ r = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test&password@",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ c1.environment.add_env("REDIS_URL", r.get_url("redis"))
+ if perms_container.has_actions():
+ perms_container.activate()
+ r.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["redis_container"]
+ assert "reservations" not in output["services"]["redis_container"]["deploy"]["resources"]
+ assert (
+ output["services"]["test_container"]["environment"]["REDIS_URL"]
+ == "redis://default:test%26password%40@redis_container:6379"
+ )
+ assert output["services"]["redis_container"]["image"] == "redis:latest"
+ assert output["services"]["redis_container"]["user"] == "1001:0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["redis_container"]["healthcheck"] == {
+ "test": "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["redis_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/bitnami/redis/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["redis_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "ALLOW_EMPTY_PASSWORD": "no",
+ "REDIS_PASSWORD": "test&password@",
+ "REDIS_PORT_NUMBER": "6379",
+ }
+ assert output["services"]["redis_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_mariadb_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.mariadb(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_mariadb(mock_values):
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ m = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["mariadb_container"]
+ assert "reservations" not in output["services"]["mariadb_container"]["deploy"]["resources"]
+ assert output["services"]["mariadb_container"]["image"] == "mariadb:latest"
+ assert output["services"]["mariadb_container"]["user"] == "999:999"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["mariadb_container"]["healthcheck"] == {
+ "test": "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["mariadb_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/mysql",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["mariadb_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "MARIADB_USER": "test_user",
+ "MARIADB_PASSWORD": "test_password",
+ "MARIADB_ROOT_PASSWORD": "test_password",
+ "MARIADB_DATABASE": "test_database",
+ "MARIADB_AUTO_UPGRADE": "true",
+ }
+ assert output["services"]["mariadb_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_perms_container(mock_values):
+ mock_values["ix_volumes"] = {
+ "test_dataset1": "/mnt/test/1",
+ "test_dataset2": "/mnt/test/2",
+ "test_dataset3": "/mnt/test/3",
+ }
+ mock_values["images"]["postgres_image"] = {"repository": "postgres", "tag": "17"}
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ # fmt: off
+ volume_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ volume_no_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ host_path_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "auto_permissions": True}}
+ host_path_no_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ host_path_acl_perms = {"type": "host_path", "host_path_config": {"acl": {"path": "/mnt/test"}, "acl_enable": True, "auto_permissions": True}} # noqa
+ ix_volume_no_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset1"}}
+ ix_volume_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset2", "auto_permissions": True}} # noqa
+ ix_volume_acl_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset3", "acl_enable": True, "auto_permissions": True}} # noqa
+ temp_volume = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ # fmt: on
+
+ c1.add_storage("/some/path1", volume_perms)
+ c1.add_storage("/some/path2", volume_no_perms)
+ c1.add_storage("/some/path3", host_path_perms)
+ c1.add_storage("/some/path4", host_path_no_perms)
+ c1.add_storage("/some/path5", host_path_acl_perms)
+ c1.add_storage("/some/path6", ix_volume_no_perms)
+ c1.add_storage("/some/path7", ix_volume_perms)
+ c1.add_storage("/some/path8", ix_volume_acl_perms)
+ c1.add_storage("/some/path9", temp_volume)
+
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data2", volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data3", host_path_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data4", host_path_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data5", host_path_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data6", ix_volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data7", ix_volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data8", ix_volume_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data9", temp_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+ postgres = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ redis = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test_password",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ mariadb = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ postgres.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ redis.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ mariadb.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert output["services"]["test_perms_container"]["network_mode"] == "none"
+ assert output["services"]["test_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+ assert output["configs"]["permissions_run_script"]["content"] != ""
+ # fmt: off
+ content = [
+ {"mount_path": "/mnt/permission/data", "is_temporary": False, "identifier": "data", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data3", "is_temporary": False, "identifier": "data3", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data6", "is_temporary": False, "identifier": "data6", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data7", "is_temporary": False, "identifier": "data7", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data9", "is_temporary": True, "identifier": "data9", "recursive": True, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/postgres_container_postgres_data", "is_temporary": False, "identifier": "postgres_container_postgres_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/redis_container_redis_data", "is_temporary": False, "identifier": "redis_container_redis_data", "recursive": False, "mode": "check", "uid": 1001, "gid": 0, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/mariadb_container_mariadb_data", "is_temporary": False, "identifier": "mariadb_container_mariadb_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ ]
+ # fmt: on
+ assert output["configs"]["permissions_actions_data"]["content"] == json.dumps(content)
+
+
+def test_add_duplicate_perms_action(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ with pytest.raises(Exception):
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+
+def test_add_perm_action_without_auto_perms_enabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": False}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "configs" not in output
+ assert "ix-test_perms_container" not in output["services"]
+ assert "depends_on" not in output["services"]["test_container"]
+
+
+def test_add_unsupported_postgres_version(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "99"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres_with_invalid_tag(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_no_upgrade_container_with_non_postgres_image(mock_values):
+ mock_values["images"]["postgres_image"] = {"repository": "tensorchord/pgvecto-rs", "tag": "pg15-v0.2.0"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert len(output["services"]) == 3 # c1, pg, perms
+ assert output["services"]["postgres_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_postgres_with_upgrade_container(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": 16.6}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ pg = output["services"]["postgres_container"]
+ pgup = output["services"]["postgres_container_upgrade"]
+ assert pg["volumes"] == pgup["volumes"]
+ assert pg["user"] == pgup["user"]
+ assert pgup["environment"]["TARGET_VERSION"] == "16"
+ assert pgup["environment"]["DATA_DIR"] == "/var/lib/postgresql/data"
+ pgup_env = pgup["environment"]
+ pgup_env.pop("TARGET_VERSION")
+ pgup_env.pop("DATA_DIR")
+ assert pg["environment"] == pgup_env
+ assert pg["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"},
+ "postgres_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert pgup["depends_on"] == {"test_perms_container": {"condition": "service_completed_successfully"}}
+ assert pgup["restart"] == "on-failure:1"
+ assert pgup["healthcheck"] == {"disable": True}
+ assert pgup["build"]["dockerfile_inline"] != ""
+ assert pgup["configs"][0]["source"] == "pg_container_upgrade.sh"
+ assert pgup["entrypoint"] == ["/bin/bash", "-c", "/upgrade.sh"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device.py
new file mode 100644
index 0000000000..2e71daa5a0
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device.py
@@ -0,0 +1,150 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
+
+
+def test_devices_without_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("", "/c/dev/sda")
+
+
+def test_devices_without_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "")
+
+
+def test_add_duplicate_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_container_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "c/dev/sda")
+
+
+def test_add_device_with_invalid_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("h/dev/sda", "/c/dev/sda")
+
+
+def test_add_disallowed_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/dri", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_cgroup_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
+
+
+def test_automatically_add_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_automatically_add_gpu_devices_and_kfd(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_remove_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.remove_devices()
+ output = render.render()
+ assert "devices" not in output["services"]["test_container"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_add_usb_bus(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_usb_bus()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
+
+
+def test_add_usb_bus_disallowed(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
+
+
+def test_add_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_add_tun_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_tun_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/net/tun:/dev/net/tun"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
new file mode 100644
index 0000000000..581fe82017
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_device_cgroup_rule(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ c1.add_device_cgroup_rule("b 10:20 rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["device_cgroup_rules"] == [
+ "b 10:20 rwm",
+ "c 13:* rwm",
+ ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 rwd")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_dns.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_dns.py
new file mode 100644
index 0000000000..fe6b21e34f
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_dns.py
@@ -0,0 +1,64 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "opt1", "opt2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_opt"] == ["attempts:3", "opt1", "opt2"]
+
+
+def test_auto_add_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_search"] == ["search1", "search2"]
+
+
+def test_auto_add_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns"] == ["nameserver1", "nameserver2"]
+
+
+def test_add_duplicate_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "attempts:5"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_environment.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_environment.py
new file mode 100644
index 0000000000..d657646582
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_environment.py
@@ -0,0 +1,196 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ mock_values["run_as"] = {"user": "1000", "group": "1000"}
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert len(envs) == 11
+ assert envs["TZ"] == "Etc/UTC"
+ assert envs["PUID"] == "1000"
+ assert envs["UID"] == "1000"
+ assert envs["USER_ID"] == "1000"
+ assert envs["PGID"] == "1000"
+ assert envs["GID"] == "1000"
+ assert envs["GROUP_ID"] == "1000"
+ assert envs["UMASK"] == "002"
+ assert envs["UMASK_SET"] == "002"
+ assert envs["NVIDIA_DRIVER_CAPABILITIES"] == "all"
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "uuid_0,uuid_1"
+
+
+def test_skip_generic_variables(mock_values):
+ mock_values["skip_generic_variables"] = True
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+
+ assert len(envs) == 1
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
+
+
+def test_add_from_all_sources(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_value")
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_value"
+ assert envs["USER_ENV"] == "test_value2"
+ assert envs["TZ"] == "Etc/UTC"
+
+
+def test_user_add_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV2", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["MY_ENV"] == "test_value"
+ assert envs["MY_ENV2"] == "test_value2"
+
+
+def test_user_add_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV", "value": "test_value2"},
+ ]
+ )
+
+
+def test_user_env_without_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "", "value": "test_value"},
+ ]
+ )
+
+
+def test_user_env_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "TZ", "value": "test_value"},
+ ]
+ )
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_user_env_try_to_overwrite_app_dev_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "PORT", "value": "test_value"},
+ ]
+ )
+ c1.environment.add_env("PORT", "test_value2")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_vars_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("TZ", "test_value")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_no_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_env("", "test_value")
+
+
+def test_app_dev_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("PORT", "test_value")
+ with pytest.raises(Exception):
+ c1.environment.add_env("PORT", "test_value2")
+
+
+def test_format_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_$value")
+ c1.environment.add_env("APP_ENV_BOOL", True)
+ c1.environment.add_env("APP_ENV_INT", 10)
+ c1.environment.add_env("APP_ENV_FLOAT", 10.5)
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_$value2"},
+ ]
+ )
+
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_$$value"
+ assert envs["USER_ENV"] == "test_$$value2"
+ assert envs["APP_ENV_BOOL"] == "true"
+ assert envs["APP_ENV_INT"] == "10"
+ assert envs["APP_ENV_FLOAT"] == "10.5"
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_expose.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_expose.py
new file mode 100644
index 0000000000..b8724d7548
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_expose.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ c1.expose.add_port(8081, "udp")
+ c1.expose.add_port(8082, "udp")
+ output = render.render()
+ assert output["services"]["test_container"]["expose"] == ["8081/tcp", "8081/udp", "8082/udp"]
+
+
+def test_add_duplicate_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ with pytest.raises(Exception):
+ c1.expose.add_port(8081)
+
+
+def test_add_expose_ports_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ output = render.render()
+ assert "expose" not in output["services"]["test_container"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_extra_hosts.py
new file mode 100644
index 0000000000..35230be16e
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_extra_hosts.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ c1.add_extra_host("test_host2", "127.0.0.2")
+ c1.add_extra_host("host.docker.internal", "host-gateway")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {
+ "host.docker.internal": "host-gateway",
+ "test_host": "127.0.0.1",
+ "test_host2": "127.0.0.2",
+ }
+
+
+def test_add_duplicate_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "::1")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "invalid_ip")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_formatter.py
new file mode 100644
index 0000000000..843cf65d2e
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_formatter.py
@@ -0,0 +1,13 @@
+from formatter import escape_dollar
+
+
+def test_escape_dollar():
+ cases = [
+ {"input": "test", "expected": "test"},
+ {"input": "$test", "expected": "$$test"},
+ {"input": "$$test", "expected": "$$$$test"},
+ {"input": "$$$test", "expected": "$$$$$$test"},
+ {"input": "$test$", "expected": "$$test$$"},
+ ]
+ for case in cases:
+ assert escape_dollar(case["input"]) == case["expected"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_functions.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_functions.py
new file mode 100644
index 0000000000..0ea3b57d18
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_functions.py
@@ -0,0 +1,88 @@
+import re
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_funcs(mock_values):
+ mock_values["ix_volumes"] = {"test": "/mnt/test123"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ tests = [
+ {"func": "auto_cast", "values": ["1"], "expected": 1},
+ {"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
+ {"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
+ {
+ "func": "bcrypt_hash",
+ "values": ["my_pass"],
+ "expect_regex": r"^\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "camel_case", "values": ["my_user"], "expected": "My_User"},
+ {"func": "copy_dict", "values": [{"a": 1}], "expected": {"a": 1}},
+ {"func": "fail", "values": ["my_message"], "expect_raise": True},
+ {
+ "func": "htpasswd",
+ "values": ["my_user", "my_pass"],
+ "expect_regex": r"^my_user:\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "is_boolean", "values": ["true"], "expected": True},
+ {"func": "is_boolean", "values": ["false"], "expected": True},
+ {"func": "is_number", "values": ["1"], "expected": True},
+ {"func": "is_number", "values": ["1.1"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[a-zA-Z0-9]+$"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[0-9]+$"], "expected": False},
+ {"func": "merge_dicts", "values": [{"a": 1}, {"b": 2}], "expected": {"a": 1, "b": 2}},
+ {"func": "must_match_regex", "values": ["my_user", "^[0-9]$"], "expect_raise": True},
+ {"func": "must_match_regex", "values": ["1", "^[0-9]$"], "expected": "1"},
+ {"func": "secure_string", "values": [10], "expect_regex": r"^[a-zA-Z0-9-_]+$"},
+ {"func": "disallow_chars", "values": ["my_user", ["$", "@"], "my_key"], "expected": "my_user"},
+ {"func": "disallow_chars", "values": ["my_user$", ["$", "@"], "my_key"], "expect_raise": True},
+ {
+ "func": "get_host_path",
+ "values": [{"type": "host_path", "host_path_config": {"path": "/mnt/test"}}],
+ "expected": "/mnt/test",
+ },
+ {
+ "func": "get_host_path",
+ "values": [{"type": "ix_volume", "ix_volume_config": {"dataset_name": "test"}}],
+ "expected": "/mnt/test123",
+ },
+ {"func": "or_default", "values": [None, 1], "expected": 1},
+ {"func": "or_default", "values": [1, None], "expected": 1},
+ {"func": "or_default", "values": [False, 1], "expected": 1},
+ {"func": "or_default", "values": [True, 1], "expected": True},
+ {"func": "temp_config", "values": [""], "expect_raise": True},
+ {
+ "func": "temp_config",
+ "values": ["test"],
+ "expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
+ },
+ ]
+
+ for test in tests:
+ print(test["func"], test)
+ func = render.funcs[test["func"]]
+ if test.get("expect_raise", False):
+ with pytest.raises(Exception):
+ func(*test["values"])
+ elif test.get("expect_regex"):
+ r = func(*test["values"])
+ assert re.match(test["expect_regex"], r) is not None
+ else:
+ r = func(*test["values"])
+ assert r == test["expected"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_healthcheck.py
new file mode 100644
index 0000000000..8fa044290f
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_healthcheck.py
@@ -0,0 +1,195 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_disable_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {"disable": True}
+
+
+def test_use_built_in_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.use_built_in()
+ output = render.render()
+ assert "healthcheck" not in output["services"]["test_container"]
+
+
+def test_set_custom_test(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test("echo $1")
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": "echo $$1",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_custom_test_array(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ c1.healthcheck.set_interval(9)
+ c1.healthcheck.set_timeout(8)
+ c1.healthcheck.set_retries(7)
+ c1.healthcheck.set_start_period(6)
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "9s",
+ "timeout": "8s",
+ "retries": 7,
+ "start_period": "6s",
+ }
+
+
+def test_adding_test_when_disabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.healthcheck.set_custom_test("echo $1")
+
+
+def test_not_adding_test(mock_values):
+ render = Render(mock_values)
+ render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_invalid_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.healthcheck.set_test("http", {"port": 8080, "path": "invalid"})
+
+
+def test_http_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("http", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == """/bin/bash -c 'exec {hc_fd}<>/dev/tcp/127.0.0.1/8080 && echo -e "GET / HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n" >&$${hc_fd} && cat <&$${hc_fd} | grep "HTTP" | grep -q "200"'""" # noqa
+ )
+
+
+def test_curl_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "curl --silent --output /dev/null --show-error --fail http://127.0.0.1:8080/health"
+ )
+
+
+def test_curl_healthcheck_with_headers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "headers": [("X-Test", "$test")]})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == 'curl --silent --output /dev/null --show-error --fail --header "X-Test: $$test" http://127.0.0.1:8080/health'
+ )
+
+
+def test_wget_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "wget --spider --quiet http://127.0.0.1:8080/health"
+ )
+
+
+def test_netcat_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("netcat", {"port": 8080})
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"]["test"] == "nc -z -w 1 127.0.0.1 8080"
+
+
+def test_tcp_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("tcp", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/8080'"
+ )
+
+
+def test_redis_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("redis")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG"
+ )
+
+
+def test_postgres_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("postgres")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB"
+ )
+
+
+def test_mariadb_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("mariadb")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping"
+ )
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_labels.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_labels.py
new file mode 100644
index 0000000000..ffa21eceac
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_labels.py
@@ -0,0 +1,88 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_disallowed_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.labels.add_label("com.docker.compose.service", "test_service")
+
+
+def test_add_duplicate_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label", "test_value")
+ with pytest.raises(Exception):
+ c1.labels.add_label("my.custom.label", "test_value1")
+
+
+def test_add_label_on_non_existing_container(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label1", "test_value1")
+ c1.labels.add_label("my.custom.label2", "test_value2")
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+
+
+def test_auto_add_labels(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ {
+ "key": "my.custom.label2",
+ "value": "test_value2",
+ "containers": ["test_container"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+ assert output["services"]["test_container2"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ }
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_notes.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_notes.py
new file mode 100644
index 0000000000..3bdfe33c74
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_notes.py
@@ -0,0 +1,184 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "ix_context": {
+ "app_metadata": {
+ "name": "test_app",
+ "title": "Test App",
+ "train": "enterprise",
+ }
+ },
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_notes(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_on_non_enterprise_train(mock_values):
+ mock_values["ix_context"]["app_metadata"]["train"] = "community"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://github.com/truenas/apps
+
+"""
+ )
+
+
+def test_notes_with_warnings(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_deprecations(mock_values):
+ render = Render(mock_values)
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_body(mock_values):
+ render = Render(mock_values)
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_all(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_portal.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_portal.py
new file mode 100644
index 0000000000..aebd9425c9
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_portal.py
@@ -0,0 +1,75 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_no_portals(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == []
+
+
+def test_add_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ render.portals.add_portal({"name": "Other Portal", "scheme": "https", "path": "/", "port": 8443})
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == [
+ {"name": "Other Portal", "scheme": "https", "host": "0.0.0.0", "port": 8443, "path": "/"},
+ {"name": "Web UI", "scheme": "http", "host": "0.0.0.0", "port": 8080, "path": "/"},
+ ]
+
+
+def test_add_duplicate_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_duplicate_portal_with_explicit_name(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_scheme(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "invalid_scheme", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_path(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "invalid_path", "port": 8080})
+
+
+def test_add_portal_with_invalid_path_double_slash(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/some//path", "port": 8080})
+
+
+def test_add_portal_with_invalid_port(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": -1})
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_ports.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_ports.py
new file mode 100644
index 0000000000..7b37a03600
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_ports.py
@@ -0,0 +1,209 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+tests = [
+ {
+ "name": "add_ports_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8082, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "add_duplicate_ports_should_fail",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_different_protocol_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_same_port_for_both_wildcard_families_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {
+ "published": 8081,
+ "target": 8080,
+ "protocol": "tcp",
+ "mode": "ingress",
+ "host_ip": "fd00:1234:5678:abcd::10",
+ },
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_with_different_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_protocol_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"protocol": "invalid_protocol"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_mode_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"mode": "invalid_mode"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "invalid_ip"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_host_port_should_fail",
+ "inputs": [
+ {"values": (-1, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_container_port_should_fail",
+ "inputs": [
+ {"values": (8081, -1), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_ports_with_different_host_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::11"}), "expect_error": False},
+ ],
+ # fmt: off
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ # fmt: on
+ },
+]
+
+
+@pytest.mark.parametrize("test", tests)
+def test_ports(test):
+ mock_values = {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ errored = False
+ for input in test["inputs"]:
+ if input["expect_error"]:
+ with pytest.raises(Exception):
+ c1.ports.add_port(*input["values"])
+ errored = True
+ else:
+ c1.ports.add_port(*input["values"])
+
+ errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False
+ if errored:
+ return
+
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == test["expected"]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_render.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_render.py
new file mode 100644
index 0000000000..60dc00679e
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_render.py
@@ -0,0 +1,37 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_values_cannot_be_modified(mock_values):
+ render = Render(mock_values)
+ render.values["test"] = "test"
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_duplicate_containers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_no_containers(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_resources.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_resources.py
new file mode 100644
index 0000000000..cd83d164e5
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_resources.py
@@ -0,0 +1,140 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_automatically_add_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": 1.0}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1.0"
+
+
+def test_invalid_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": 1024}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "1024M"
+
+
+def test_invalid_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_gpus(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_0", "uuid_1"],
+ }
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_gpu_without_uuid(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_remove_cpus_and_memory_with_gpus(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_1", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "limits" not in output["services"]["test_container"]["deploy"]["resources"]
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_1"],
+ }
+
+
+def test_remove_cpus_and_memory(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "deploy" not in output["services"]["test_container"]
+
+
+def test_remove_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_devices()
+ output = render.render()
+ assert "reservations" not in output["services"]["test_container"]["deploy"]["resources"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_set_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.set_profile("low")
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1"
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "512M"
+
+
+def test_set_profile_invalid_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.deploy.resources.set_profile("invalid_profile")
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_restart.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_restart.py
new file mode 100644
index 0000000000..06b2975590
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_restart.py
@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_invalid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("invalid_policy")
+
+
+def test_valid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure")
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure"
+
+
+def test_valid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure", 10)
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure:10"
+
+
+def test_invalid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("on-failure", maximum_retry_count=-1)
+
+
+def test_invalid_restart_policy_with_maximum_retry_count_and_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("always", maximum_retry_count=10)
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_sysctls.py
new file mode 100644
index 0000000000..c9414044ea
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_sysctls.py
@@ -0,0 +1,62 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ c1.sysctls.add("fs.mqueue.msg_max", 100)
+ output = render.render()
+ assert output["services"]["test_container"]["sysctls"] == {"net.ipv4.ip_forward": "1", "fs.mqueue.msg_max": "100"}
+
+
+def test_add_net_sysctl_with_host_network(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_duplicate_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ c1.sysctls.add("net.ipv4.ip_forward", 0)
+
+
+def test_add_empty_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.sysctls.add("", 1)
+
+
+def test_add_sysctl_with_invalid_key(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("invalid.sysctl", 1)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_validations.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_validations.py
new file mode 100644
index 0000000000..f0986ce9a5
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/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/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_volumes.py
new file mode 100644
index 0000000000..9a98956bdc
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/tests/test_volumes.py
@@ -0,0 +1,727 @@
+import pytest
+
+
+from render import Render
+from formatter import get_hashed_name_for_volume
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_volume_invalid_type(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "invalid_type"})
+
+
+def test_add_volume_empty_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("", {"type": "tmpfs"})
+
+
+def test_add_volume_duplicate_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+
+
+def test_add_volume_host_path_invalid_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_host_path_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_with_acl_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_mount(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_acl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
+ }
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test/acl",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "slave"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_create_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_invalid_dataset_name(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_no_ix_volume_config(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_volume_mount(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_volume_mount_with_options(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {
+ "type": "ix_volume",
+ "ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
+ }
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rslave"},
+ }
+ ]
+
+
+def test_cifs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_username(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_password(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_without_cifs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["verbose=true", "verbose=true"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["user=username"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": {"verbose": True},
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": [{"verbose": True}],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_add_cifs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_cifs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["vers=3.0", "verbose=true"],
+ }
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "cifs",
+ "device": "//server/path",
+ "o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_without_nfs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {
+ "type": "nfs",
+ "nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_add_nfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path"}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "nfs",
+ "device": ":/path",
+ "o": "addr=server,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_tmpfs_invalid_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_zero_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_invalid_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs"}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "tmpfs",
+ "target": "/some/path",
+ "read_only": False,
+ }
+ ]
+
+
+def test_temporary_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "source": "test_temp_volume",
+ "type": "volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+
+
+def test_docker_volume_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume_missing_volume_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["volumes"] == {"test_volume": {}}
+
+
+def test_anonymous_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
+ ]
+ assert "volumes" not in output
+
+
+def test_add_udev(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev()
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_udev_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev(read_only=False)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/some/path")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_host_path_with_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_host_path_without_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/mnt", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/mnt",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/validations.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/validations.py
new file mode 100644
index 0000000000..d4aa633006
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/validations.py
@@ -0,0 +1,316 @@
+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_port_bind_mode_or_raise(status: str):
+ valid_statuses = ("published", "exposed", "")
+ if status not in valid_statuses:
+ raise RenderError(f"Invalid port status [{status}]")
+ return status
+
+
+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_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+ valid_modes = ("", "host", "private", "shareable", "none")
+ if ipc_mode in valid_modes:
+ return ipc_mode
+ if ipc_mode.startswith("container:"):
+ if ipc_mode[10:] not in containers:
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+ return ipc_mode
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+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 valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+ parts = dev_grp_rule.split(" ")
+ if len(parts) != 3:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [ : ]"
+ )
+
+ valid_types = ("a", "b", "c")
+ if parts[0] not in valid_types:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+ f" but got [{parts[0]}]"
+ )
+
+ major, minor = parts[1].split(":")
+ for part in (major, minor):
+ if part != "*" and not part.isdigit():
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+ f" or [*] but got [{major}] and [{minor}]"
+ )
+
+ valid_cgroup_perm_or_raise(parts[2])
+
+ return dev_grp_rule
+
+
+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/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount.py
new file mode 100644
index 0000000000..aadd077750
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount.py
@@ -0,0 +1,92 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .formatter import merge_dicts_no_overwrite
+ from .volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from .volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+except ImportError:
+ from error import RenderError
+ from formatter import merge_dicts_no_overwrite
+ from volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+
+
+class VolumeMount:
+ def __init__(self, render_instance: "Render", mount_path: str, config: "IxStorage"):
+ self._render_instance = render_instance
+ self.mount_path: str = mount_path
+
+ storage_type: str = config.get("type", "")
+ if not storage_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match storage_type:
+ case "host_path":
+ spec_type = "bind"
+ mount_config = config.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = HostPathSource(self._render_instance, mount_config).get()
+ case "ix_volume":
+ spec_type = "bind"
+ mount_config = config.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = IxVolumeSource(self._render_instance, mount_config).get()
+ case "tmpfs":
+ spec_type = "tmpfs"
+ mount_config = config.get("tmpfs_config", {})
+ mount_type_specific_definition = TmpfsMountType(self._render_instance, mount_config).render()
+ source = None
+ case "nfs":
+ spec_type = "volume"
+ mount_config = config.get("nfs_config")
+ if mount_config is None:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = NfsSource(self._render_instance, mount_config).get()
+ case "cifs":
+ spec_type = "volume"
+ mount_config = config.get("cifs_config")
+ if mount_config is None:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = CifsSource(self._render_instance, mount_config).get()
+ case "volume":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [volume] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "temporary":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [temporary] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "anonymous":
+ spec_type = "volume"
+ mount_config = config.get("volume_config") or {}
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = None
+ case _:
+ raise RenderError(f"Storage type [{storage_type}] is not supported for volume mounts.")
+
+ common_spec = {"type": spec_type, "target": self.mount_path, "read_only": config.get("read_only", False)}
+ if source is not None:
+ common_spec["source"] = source
+ self._render_instance.volumes.add_volume(source, storage_type, mount_config) # type: ignore
+
+ self.volume_mount_spec = merge_dicts_no_overwrite(common_spec, mount_type_specific_definition)
+
+ def render(self) -> dict:
+ return self.volume_mount_spec
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount_types.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount_types.py
new file mode 100644
index 0000000000..00a0ec3a18
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_mount_types.py
@@ -0,0 +1,72 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageTmpfsConfig, IxStorageVolumeConfig, IxStorageBindLikeConfigs
+
+
+try:
+ from .error import RenderError
+ from .validations import valid_host_path_propagation, valid_octal_mode_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_host_path_propagation, valid_octal_mode_or_raise
+
+
+class TmpfsMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageTmpfsConfig"):
+ self._render_instance = render_instance
+ self.spec = {"tmpfs": {}}
+ size = config.get("size", None)
+ mode = config.get("mode", None)
+
+ if size is not None:
+ if not isinstance(size, int):
+ raise RenderError(f"Expected [size] to be an integer for [tmpfs] type, got [{size}]")
+ if not size > 0:
+ raise RenderError(f"Expected [size] to be greater than 0 for [tmpfs] type, got [{size}]")
+ # Convert Mebibytes to Bytes
+ self.spec["tmpfs"]["size"] = size * 1024 * 1024
+
+ if mode is not None:
+ mode = valid_octal_mode_or_raise(mode)
+ self.spec["tmpfs"]["mode"] = int(mode, 8)
+
+ if not self.spec["tmpfs"]:
+ self.spec.pop("tmpfs")
+
+ def render(self) -> dict:
+ """Render the tmpfs mount specification."""
+ return self.spec
+
+
+class BindMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageBindLikeConfigs"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ propagation = valid_host_path_propagation(config.get("propagation", "rprivate"))
+ create_host_path = config.get("create_host_path", False)
+
+ self.spec: dict = {
+ "bind": {
+ "create_host_path": create_host_path,
+ "propagation": propagation,
+ }
+ }
+
+ def render(self) -> dict:
+ """Render the bind mount specification."""
+ return self.spec
+
+
+class VolumeMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ self.spec: dict = {"volume": {"nocopy": config.get("nocopy", False)}}
+
+ def render(self) -> dict:
+ """Render the volume mount specification."""
+ return self.spec
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_sources.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_sources.py
new file mode 100644
index 0000000000..6aba23df23
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_sources.py
@@ -0,0 +1,110 @@
+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("/")
+ # TODO: Hack for Nextcloud deprecated config. Remove once we remove support for it
+ allow_unsafe_ix_volume = config.get("allow_unsafe_ix_volume", False)
+ self.source = allowed_fs_host_path_or_raise(path, allow_unsafe_ix_volume)
+
+ 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/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_types.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_types.py
new file mode 100644
index 0000000000..4ccea08f83
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volume_types.py
@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageNfsConfig, IxStorageCifsConfig, IxStorageVolumeConfig
+
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_fs_path_or_raise
+
+
+class NfsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageNfsConfig"):
+ self._render_instance = render_instance
+
+ if not config:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type")
+
+ required_keys = ["server", "path"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [nfs] type")
+
+ opts = [f"addr={config['server']}"]
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [nfs_config.options] to be a list for [nfs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["addr"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [nfs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [nfs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [nfs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+
+ opts.sort()
+
+ path = valid_fs_path_or_raise(config["path"].rstrip("/"))
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "nfs",
+ "device": f":{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class CifsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageCifsConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ if not config:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type")
+
+ required_keys = ["server", "path", "username", "password"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [cifs] type")
+
+ opts = [
+ "noperm",
+ f"user={config['username']}",
+ f"password={config['password']}",
+ ]
+
+ domain = config.get("domain")
+ if domain:
+ opts.append(f"domain={domain}")
+
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [cifs_config.options] to be a list for [cifs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["user", "password", "domain", "noperm"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [cifs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [cifs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ for disallowed in disallowed_opts:
+ if key == disallowed:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+ opts.sort()
+
+ server = config["server"].lstrip("/")
+ path = config["path"].strip("/")
+ path = valid_fs_path_or_raise("/" + path).lstrip("/")
+
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "cifs",
+ "device": f"//{server}/{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class DockerVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ def get(self):
+ return self.volume_spec
diff --git a/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volumes.py b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volumes.py
new file mode 100644
index 0000000000..e6925a402f
--- /dev/null
+++ b/trains/stable/collabora/1.2.13/templates/library/base_v2_1_14/volumes.py
@@ -0,0 +1,66 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+try:
+ from .error import RenderError
+ from .storage import IxStorageVolumeLikeConfigs
+ from .volume_types import NfsVolume, CifsVolume, DockerVolume
+except ImportError:
+ from error import RenderError
+ from storage import IxStorageVolumeLikeConfigs
+ from volume_types import NfsVolume, CifsVolume, DockerVolume
+
+
+class Volumes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volumes: dict[str, Volume] = {}
+
+ def add_volume(
+ self,
+ source: str,
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ # This method can be called many times from the volume mounts
+ # Only add the volume if it is not already added, but dont raise an error
+ if source == "":
+ raise RenderError(f"Volume source [{source}] cannot be empty")
+
+ if source in self._volumes:
+ return
+
+ self._volumes[source] = Volume(self._render_instance, storage_type, config)
+
+ def has_volumes(self) -> bool:
+ return bool(self._volumes)
+
+ def render(self):
+ return {name: v.render() for name, v in sorted(self._volumes.items()) if v.render() is not None}
+
+
+class Volume:
+ def __init__(
+ self,
+ render_instance: "Render",
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ self._render_instance = render_instance
+ self.volume_spec: dict | None = {}
+
+ match storage_type:
+ case "nfs":
+ self.volume_spec = NfsVolume(self._render_instance, config).get() # type: ignore
+ case "cifs":
+ self.volume_spec = CifsVolume(self._render_instance, config).get() # type: ignore
+ case "volume" | "temporary":
+ self.volume_spec = DockerVolume(self._render_instance, config).get() # type: ignore
+ case _:
+ self.volume_spec = None
+
+ def render(self):
+ return self.volume_spec
diff --git a/trains/stable/collabora/1.2.12/templates/macros/nginx.conf.jinja b/trains/stable/collabora/1.2.13/templates/macros/nginx.conf.jinja
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/macros/nginx.conf.jinja
rename to trains/stable/collabora/1.2.13/templates/macros/nginx.conf.jinja
diff --git a/trains/stable/collabora/1.2.12/templates/test_values/basic-values.yaml b/trains/stable/collabora/1.2.13/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/test_values/basic-values.yaml
rename to trains/stable/collabora/1.2.13/templates/test_values/basic-values.yaml
diff --git a/trains/stable/collabora/1.2.12/templates/test_values/https-values.yaml b/trains/stable/collabora/1.2.13/templates/test_values/https-values.yaml
similarity index 100%
rename from trains/stable/collabora/1.2.12/templates/test_values/https-values.yaml
rename to trains/stable/collabora/1.2.13/templates/test_values/https-values.yaml
diff --git a/trains/stable/collabora/app_versions.json b/trains/stable/collabora/app_versions.json
index ca17196d90..6ef1a88635 100644
--- a/trains/stable/collabora/app_versions.json
+++ b/trains/stable/collabora/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.2.12": {
+ "1.2.13": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/stable/collabora/1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/stable/collabora/1.2.13",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "24.04.12.1.1_1.2.12",
- "version": "1.2.12",
+ "human_version": "24.04.12.2.1_1.2.13",
+ "version": "1.2.13",
"app_metadata": {
- "app_version": "24.04.12.1.1",
+ "app_version": "24.04.12.2.1",
"capabilities": [
{
"description": "Collabora and Nginx are able to chown files.",
@@ -92,7 +92,7 @@
],
"title": "Collabora",
"train": "stable",
- "version": "1.2.12"
+ "version": "1.2.13"
},
"schema": {
"groups": [
diff --git a/trains/stable/diskoverdata/app_versions.json b/trains/stable/diskoverdata/app_versions.json
index f5d21fa2b6..aea57d576a 100644
--- a/trains/stable/diskoverdata/app_versions.json
+++ b/trains/stable/diskoverdata/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/diskoverdata/1.4.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2.3.0_1.4.9",
"version": "1.4.9",
diff --git a/trains/stable/elastic-search/app_versions.json b/trains/stable/elastic-search/app_versions.json
index bf7abbaabc..69d92c2b8d 100644
--- a/trains/stable/elastic-search/app_versions.json
+++ b/trains/stable/elastic-search/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/elastic-search/1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "8.17.1_1.2.9",
"version": "1.2.9",
diff --git a/trains/stable/emby/app_versions.json b/trains/stable/emby/app_versions.json
index 3ad4d13a91..747306c0f6 100644
--- a/trains/stable/emby/app_versions.json
+++ b/trains/stable/emby/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/emby/1.2.15",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "4.8.10.0_1.2.15",
"version": "1.2.15",
diff --git a/trains/stable/home-assistant/app_versions.json b/trains/stable/home-assistant/app_versions.json
index d8586cf575..e2ead1b6c7 100644
--- a/trains/stable/home-assistant/app_versions.json
+++ b/trains/stable/home-assistant/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/home-assistant/1.4.18",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2025.1.4_1.4.18",
"version": "1.4.18",
diff --git a/trains/stable/ix-app/app_versions.json b/trains/stable/ix-app/app_versions.json
index 1d9e9ff93e..74a6c83323 100644
--- a/trains/stable/ix-app/app_versions.json
+++ b/trains/stable/ix-app/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/ix-app/1.1.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.0.0_1.1.9",
"version": "1.1.9",
diff --git a/trains/stable/minio/app_versions.json b/trains/stable/minio/app_versions.json
index ba08ee4bed..355dadae5d 100644
--- a/trains/stable/minio/app_versions.json
+++ b/trains/stable/minio/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/minio/1.2.12",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "RELEASE.2025-01-20T14-49-07Z_1.2.12",
"version": "1.2.12",
diff --git a/trains/stable/netdata/1.2.11/README.md b/trains/stable/netdata/1.2.12/README.md
similarity index 100%
rename from trains/stable/netdata/1.2.11/README.md
rename to trains/stable/netdata/1.2.12/README.md
diff --git a/trains/stable/netdata/1.2.11/app.yaml b/trains/stable/netdata/1.2.12/app.yaml
similarity index 98%
rename from trains/stable/netdata/1.2.11/app.yaml
rename to trains/stable/netdata/1.2.12/app.yaml
index cf2f91d7cf..a7a9b9708b 100644
--- a/trains/stable/netdata/1.2.11/app.yaml
+++ b/trains/stable/netdata/1.2.12/app.yaml
@@ -1,4 +1,4 @@
-app_version: v2.2.1
+app_version: v2.2.2
capabilities:
- description: Netdata is able to chown files.
name: CHOWN
@@ -58,4 +58,4 @@ sources:
- https://github.com/netdata/netdata
title: Netdata
train: stable
-version: 1.2.11
+version: 1.2.12
diff --git a/trains/stable/netdata/1.2.11/ix_values.yaml b/trains/stable/netdata/1.2.12/ix_values.yaml
similarity index 85%
rename from trains/stable/netdata/1.2.11/ix_values.yaml
rename to trains/stable/netdata/1.2.12/ix_values.yaml
index 7bccc35c58..9c846ebe35 100644
--- a/trains/stable/netdata/1.2.11/ix_values.yaml
+++ b/trains/stable/netdata/1.2.12/ix_values.yaml
@@ -1,7 +1,7 @@
images:
image:
repository: netdata/netdata
- tag: v2.2.1
+ tag: v2.2.2
consts:
netdata_container_name: netdata
diff --git a/trains/stable/netdata/1.2.11/migrations/migrate_from_kubernetes b/trains/stable/netdata/1.2.12/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migrate_from_kubernetes
rename to trains/stable/netdata/1.2.12/migrations/migrate_from_kubernetes
diff --git a/trains/stable/netdata/1.2.12/migrations/migration_helpers/__init__.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/cpu.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/cpu.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/cpu.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/dns_config.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/dns_config.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/dns_config.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/kubernetes_secrets.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/memory.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/memory.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/memory.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/resources.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/resources.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/resources.py
diff --git a/trains/stable/netdata/1.2.11/migrations/migration_helpers/storage.py b/trains/stable/netdata/1.2.12/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/stable/netdata/1.2.11/migrations/migration_helpers/storage.py
rename to trains/stable/netdata/1.2.12/migrations/migration_helpers/storage.py
diff --git a/trains/stable/netdata/1.2.11/questions.yaml b/trains/stable/netdata/1.2.12/questions.yaml
similarity index 100%
rename from trains/stable/netdata/1.2.11/questions.yaml
rename to trains/stable/netdata/1.2.12/questions.yaml
diff --git a/trains/stable/netdata/1.2.11/templates/docker-compose.yaml b/trains/stable/netdata/1.2.12/templates/docker-compose.yaml
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/docker-compose.yaml
rename to trains/stable/netdata/1.2.12/templates/docker-compose.yaml
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/__init__.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/configs.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/configs.py
new file mode 100644
index 0000000000..b76f4b169c
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/configs.py
@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._configs: dict[str, dict] = {}
+
+ def add(self, name: str, data: str):
+ if not isinstance(data, str):
+ raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+ if name not in self._configs:
+ self._configs[name] = {"name": name, "data": data}
+ return
+
+ if data == self._configs[name]["data"]:
+ return
+
+ raise RenderError(f"Config [{name}] already added with different data")
+
+ def has_configs(self):
+ return bool(self._configs)
+
+ def render(self):
+ return {
+ c["name"]: {"content": escape_dollar(c["data"])}
+ for c in sorted(self._configs.values(), key=lambda c: c["name"])
+ }
+
+
+class ContainerConfigs:
+ def __init__(self, render_instance: "Render", configs: Configs):
+ self._render_instance = render_instance
+ self.top_level_configs: Configs = configs
+ self.container_configs: set[ContainerConfig] = set()
+
+ def add(self, name: str, data: str, target: str, mode: str = ""):
+ self.top_level_configs.add(name, data)
+
+ if target == "":
+ raise RenderError(f"Expected [target] to be set for config [{name}]")
+ if mode != "":
+ mode = valid_octal_mode_or_raise(mode)
+
+ if target in [c.target for c in self.container_configs]:
+ raise RenderError(f"Target [{target}] already used for another config")
+ target = valid_fs_path_or_raise(target)
+ self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+ def has_configs(self):
+ return bool(self.container_configs)
+
+ def render(self):
+ return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+ def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+ self._render_instance = render_instance
+ self.source = source
+ self.target = target
+ self.mode = mode
+
+ def render(self):
+ result: dict[str, str | int] = {
+ "source": self.source,
+ "target": self.target,
+ }
+
+ if self.mode:
+ result["mode"] = int(self.mode, 8)
+
+ return result
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/container.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/container.py
new file mode 100644
index 0000000000..8e889be045
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/container.py
@@ -0,0 +1,418 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .configs import ContainerConfigs
+ from .depends import Depends
+ from .deploy import Deploy
+ from .device_cgroup_rules import DeviceCGroupRules
+ from .devices import Devices
+ from .dns import Dns
+ from .environment import Environment
+ from .error import RenderError
+ from .expose import Expose
+ from .extra_hosts import ExtraHosts
+ from .formatter import escape_dollar, get_image_with_hashed_data
+ from .healthcheck import Healthcheck
+ from .labels import Labels
+ from .ports import Ports
+ from .restart import RestartPolicy
+ from .validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from .storage import Storage
+ from .sysctls import Sysctls
+except ImportError:
+ from configs import ContainerConfigs
+ from depends import Depends
+ from deploy import Deploy
+ from device_cgroup_rules import DeviceCGroupRules
+ from devices import Devices
+ from dns import Dns
+ from environment import Environment
+ from error import RenderError
+ from expose import Expose
+ from extra_hosts import ExtraHosts
+ from formatter import escape_dollar, get_image_with_hashed_data
+ from healthcheck import Healthcheck
+ from labels import Labels
+ from ports import Ports
+ from restart import RestartPolicy
+ from validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from storage import Storage
+ from sysctls import Sysctls
+
+
+class Container:
+ def __init__(self, render_instance: "Render", name: str, image: str):
+ self._render_instance = render_instance
+
+ self._name: str = name
+ self._image: str = self._resolve_image(image)
+ self._build_image: str = ""
+ self._pull_policy: str = ""
+ self._user: str = ""
+ self._tty: bool = False
+ self._stdin_open: bool = False
+ self._init: bool | None = None
+ self._read_only: bool | None = None
+ self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+ self._hostname: str = ""
+ self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
+ self._cap_add: set[str] = set()
+ self._security_opt: set[str] = set(["no-new-privileges"])
+ self._privileged: bool = False
+ self._group_add: set[int | str] = set()
+ self._network_mode: str = ""
+ self._entrypoint: list[str] = []
+ self._command: list[str] = []
+ self._grace_period: int | None = None
+ self._shm_size: int | None = None
+ self._storage: Storage = Storage(self._render_instance)
+ self._ipc_mode: str | None = None
+ self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+ self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+ self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+ self.deploy: Deploy = Deploy(self._render_instance)
+ self.networks: set[str] = set()
+ self.devices: Devices = Devices(self._render_instance)
+ self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+ self.dns: Dns = Dns(self._render_instance)
+ self.depends: Depends = Depends(self._render_instance)
+ self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+ self.labels: Labels = Labels(self._render_instance)
+ self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+ self.ports: Ports = Ports(self._render_instance)
+ self.expose: Expose = Expose(self._render_instance)
+
+ self._auto_set_network_mode()
+ self._auto_add_labels()
+ self._auto_add_groups()
+
+ def _auto_add_groups(self):
+ self.add_group(568)
+
+ def _auto_set_network_mode(self):
+ if self._render_instance.values.get("network", {}).get("host_network", False):
+ self.set_network_mode("host")
+
+ def _auto_add_labels(self):
+ labels = self._render_instance.values.get("labels", [])
+ if not labels:
+ return
+
+ for label in labels:
+ containers = label.get("containers", [])
+ if not containers:
+ raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+ if self._name in containers:
+ self.labels.add_label(label["key"], label["value"])
+
+ def _resolve_image(self, image: str):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(
+ f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+ )
+ repo = images[image].get("repository", "")
+ tag = images[image].get("tag", "")
+
+ if not repo:
+ raise RenderError(f"Repository not found for image [{image}]")
+ if not tag:
+ raise RenderError(f"Tag not found for image [{image}]")
+
+ return f"{repo}:{tag}"
+
+ def build_image(self, content: list[str | None]):
+ dockerfile = f"FROM {self._image}\n"
+ for line in content:
+ if not line:
+ continue
+ if line.startswith("FROM"):
+ # TODO: This will also block multi-stage builds
+ # We can revisit this later if we need it
+ raise RenderError(
+ "FROM cannot be used in build image. Define the base image when creating the container."
+ )
+ dockerfile += line + "\n"
+
+ self._build_image = dockerfile
+ self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+ def set_pull_policy(self, pull_policy: str):
+ self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+ def set_user(self, user: int, group: int):
+ for i in (user, group):
+ if not isinstance(i, int) or i < 0:
+ raise RenderError(f"User/Group [{i}] is not valid")
+ self._user = f"{user}:{group}"
+
+ def add_extra_host(self, host: str, ip: str):
+ self._extra_hosts.add_host(host, ip)
+
+ def add_group(self, group: int | str):
+ if isinstance(group, str):
+ group = str(group).strip()
+ if group.isdigit():
+ raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+ if group in self._group_add:
+ raise RenderError(f"Group [{group}] already added")
+ self._group_add.add(group)
+
+ def get_additional_groups(self) -> list[int | str]:
+ result = []
+ if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+ result.append(44) # video
+ result.append(107) # render
+ return result
+
+ def get_current_groups(self) -> list[str]:
+ result = [str(g) for g in self._group_add]
+ result.extend([str(g) for g in self.get_additional_groups()])
+ return result
+
+ def set_tty(self, enabled: bool = False):
+ self._tty = enabled
+
+ def set_stdin(self, enabled: bool = False):
+ self._stdin_open = enabled
+
+ def set_ipc_mode(self, ipc_mode: str):
+ self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+ def add_device_cgroup_rule(self, dev_grp_rule: str):
+ self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+ def set_init(self, enabled: bool = False):
+ self._init = enabled
+
+ def set_read_only(self, enabled: bool = False):
+ self._read_only = enabled
+
+ def set_hostname(self, hostname: str):
+ self._hostname = hostname
+
+ def set_grace_period(self, grace_period: int):
+ if grace_period < 0:
+ raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+ self._grace_period = grace_period
+
+ def set_privileged(self, enabled: bool = False):
+ self._privileged = enabled
+
+ def clear_caps(self):
+ self._cap_add.clear()
+ self._cap_drop.clear()
+
+ def add_caps(self, caps: list[str]):
+ for c in caps:
+ if c in self._cap_add:
+ raise RenderError(f"Capability [{c}] already added")
+ self._cap_add.add(valid_cap_or_raise(c))
+
+ def add_security_opt(self, opt: str):
+ if opt in self._security_opt:
+ raise RenderError(f"Security Option [{opt}] already added")
+ self._security_opt.add(opt)
+
+ def remove_security_opt(self, opt: str):
+ self._security_opt.remove(opt)
+
+ def set_network_mode(self, mode: str):
+ self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+ def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+ port_config = port_config or {}
+ dev_config = dev_config or {}
+ # Merge port_config and dev_config (dev_config has precedence)
+ config = port_config | dev_config
+
+ bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+ # Skip port if its neither published nor exposed
+ if not bind_mode:
+ return
+
+ # Collect port config
+ host_port = config.get("port_number", 0)
+ container_port = config.get("container_port", 0) or host_port
+ protocol = config.get("protocol", "tcp")
+ host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+ if not isinstance(host_ips, list):
+ raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+ if bind_mode == "published":
+ for host_ip in host_ips:
+ self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip})
+ elif bind_mode == "exposed":
+ self.expose.add_port(container_port, protocol)
+
+ def set_entrypoint(self, entrypoint: list[str]):
+ self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+ def set_command(self, command: list[str]):
+ self._command = [escape_dollar(str(e)) for e in command]
+
+ def add_storage(self, mount_path: str, config: "IxStorage"):
+ self._storage.add(mount_path, config)
+
+ def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+ self.add_group(999)
+ self._storage._add_docker_socket(read_only, mount_path)
+
+ def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+ self._storage._add_udev(read_only, mount_path)
+
+ def add_tun_device(self):
+ self.devices._add_tun_device()
+
+ def add_snd_device(self):
+ self.add_group(29)
+ self.devices._add_snd_device()
+
+ def set_shm_size_mb(self, size: int):
+ self._shm_size = size
+
+ # Easily remove devices from the container
+ # Useful in dependencies like postgres and redis
+ # where there is no need to pass devices to them
+ def remove_devices(self):
+ self.deploy.resources.remove_devices()
+ self.devices.remove_devices()
+
+ @property
+ def storage(self):
+ return self._storage
+
+ def render(self) -> dict[str, Any]:
+ if self._network_mode and self.networks:
+ raise RenderError("Cannot set both [network_mode] and [networks]")
+
+ result = {
+ "image": self._image,
+ "platform": "linux/amd64",
+ "tty": self._tty,
+ "stdin_open": self._stdin_open,
+ "restart": self.restart.render(),
+ }
+
+ if self._pull_policy:
+ result["pull_policy"] = self._pull_policy
+
+ if self.healthcheck.has_healthcheck():
+ result["healthcheck"] = self.healthcheck.render()
+
+ if self._hostname:
+ result["hostname"] = self._hostname
+
+ if self._build_image:
+ result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ if self._ipc_mode is not None:
+ result["ipc"] = self._ipc_mode
+
+ if self._device_cgroup_rules.has_rules():
+ result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+ if self._extra_hosts.has_hosts():
+ result["extra_hosts"] = self._extra_hosts.render()
+
+ if self._init is not None:
+ result["init"] = self._init
+
+ if self._read_only is not None:
+ result["read_only"] = self._read_only
+
+ if self._grace_period is not None:
+ result["stop_grace_period"] = f"{self._grace_period}s"
+
+ if self._user:
+ result["user"] = self._user
+
+ for g in self.get_additional_groups():
+ self.add_group(g)
+
+ if self._group_add:
+ result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+ if self._shm_size is not None:
+ result["shm_size"] = f"{self._shm_size}M"
+
+ if self._privileged is not None:
+ result["privileged"] = self._privileged
+
+ if self._cap_drop:
+ result["cap_drop"] = sorted(self._cap_drop)
+
+ if self._cap_add:
+ result["cap_add"] = sorted(self._cap_add)
+
+ if self._security_opt:
+ result["security_opt"] = sorted(self._security_opt)
+
+ if self._network_mode:
+ result["network_mode"] = self._network_mode
+
+ if self.sysctls.has_sysctls():
+ result["sysctls"] = self.sysctls.render()
+
+ if self._network_mode != "host":
+ if self.ports.has_ports():
+ result["ports"] = self.ports.render()
+
+ if self.expose.has_ports():
+ result["expose"] = self.expose.render()
+
+ if self._entrypoint:
+ result["entrypoint"] = self._entrypoint
+
+ if self._command:
+ result["command"] = self._command
+
+ if self.devices.has_devices():
+ result["devices"] = self.devices.render()
+
+ if self.deploy.has_deploy():
+ result["deploy"] = self.deploy.render()
+
+ if self.environment.has_variables():
+ result["environment"] = self.environment.render()
+
+ if self.labels.has_labels():
+ result["labels"] = self.labels.render()
+
+ if self.dns.has_dns_nameservers():
+ result["dns"] = self.dns.render_dns_nameservers()
+
+ if self.dns.has_dns_searches():
+ result["dns_search"] = self.dns.render_dns_searches()
+
+ if self.dns.has_dns_opts():
+ result["dns_opt"] = self.dns.render_dns_opts()
+
+ if self.depends.has_dependencies():
+ result["depends_on"] = self.depends.render()
+
+ if self._storage.has_mounts():
+ result["volumes"] = self._storage.render()
+
+ return result
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/depends.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/depends.py
new file mode 100644
index 0000000000..4e057cf085
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/depends.py
@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_depend_condition_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dependencies: dict[str, str] = {}
+
+ def add_dependency(self, name: str, condition: str):
+ condition = valid_depend_condition_or_raise(condition)
+ if name in self._dependencies.keys():
+ raise RenderError(f"Dependency [{name}] already added")
+ if name not in self._render_instance.container_names():
+ raise RenderError(
+ f"Dependency [{name}] not found in defined containers. "
+ f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+ )
+ self._dependencies[name] = condition
+
+ def has_dependencies(self):
+ return len(self._dependencies) > 0
+
+ def render(self):
+ return {d: {"condition": c} for d, c in self._dependencies.items()}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deploy.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deploy.py
new file mode 100644
index 0000000000..894dbc643b
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deploy.py
@@ -0,0 +1,24 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .resources import Resources
+except ImportError:
+ from resources import Resources
+
+
+class Deploy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self.resources: Resources = Resources(self._render_instance)
+
+ def has_deploy(self):
+ return self.resources.has_resources()
+
+ def render(self):
+ if self.resources.has_resources():
+ return {"resources": self.resources.render()}
+
+ return {}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps.py
new file mode 100644
index 0000000000..e96849f979
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps.py
@@ -0,0 +1,32 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .deps_postgres import PostgresContainer, PostgresConfig
+ from .deps_redis import RedisContainer, RedisConfig
+ from .deps_mariadb import MariadbContainer, MariadbConfig
+ from .deps_perms import PermsContainer
+except ImportError:
+ from deps_postgres import PostgresContainer, PostgresConfig
+ from deps_redis import RedisContainer, RedisConfig
+ from deps_mariadb import MariadbContainer, MariadbConfig
+ from deps_perms import PermsContainer
+
+
+class Deps:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def perms(self, name: str):
+ return PermsContainer(self._render_instance, name)
+
+ def postgres(self, name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer):
+ return PostgresContainer(self._render_instance, name, image, config, perms_instance)
+
+ def redis(self, name: str, image: str, config: RedisConfig, perms_instance: PermsContainer):
+ return RedisContainer(self._render_instance, name, image, config, perms_instance)
+
+ def mariadb(self, name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer):
+ return MariadbContainer(self._render_instance, name, image, config, perms_instance)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py
new file mode 100644
index 0000000000..baf863b370
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_mariadb.py
@@ -0,0 +1,65 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class MariadbConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ root_password: NotRequired[str]
+ port: NotRequired[int]
+ auto_upgrade: NotRequired[bool]
+ volume: "IxStorage"
+
+
+class MariadbContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for mariadb")
+
+ port = valid_port_or_raise(config.get("port") or 3306)
+ root_password = config.get("root_password") or config["password"]
+ auto_upgrade = config.get("auto_upgrade", True)
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(999, 999)
+ c.healthcheck.set_test("mariadb")
+ c.remove_devices()
+
+ c.add_storage("/var/lib/mysql", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_mariadb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ c.environment.add_env("MARIADB_USER", config["user"])
+ c.environment.add_env("MARIADB_PASSWORD", config["password"])
+ c.environment.add_env("MARIADB_ROOT_PASSWORD", root_password)
+ c.environment.add_env("MARIADB_DATABASE", config["database"])
+ c.environment.add_env("MARIADB_AUTO_UPGRADE", str(auto_upgrade).lower())
+ c.set_command(["--port", str(port)])
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_perms.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_perms.py
new file mode 100644
index 0000000000..cdc5a3820a
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_perms.py
@@ -0,0 +1,252 @@
+import json
+import pathlib
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class PermsContainer:
+ def __init__(self, render_instance: "Render", name: str):
+ self._render_instance = render_instance
+ self._name = name
+ self.actions: set[str] = set()
+ self.parsed_configs: list[dict] = []
+
+ def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ identifier = self.normalize_identifier_for_path(identifier)
+ if identifier in self.actions:
+ raise RenderError(f"Action with id [{identifier}] already used for another permission action")
+
+ parsed_action = self.parse_action(identifier, volume_config, action_config)
+ if parsed_action:
+ self.parsed_configs.append(parsed_action)
+ self.actions.add(identifier)
+
+ def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ valid_modes = [
+ "always", # Always set permissions, without checking.
+ "check", # Checks if permissions are correct, and set them if not.
+ ]
+ mode = action_config.get("mode", "check")
+ uid = action_config.get("uid", None)
+ gid = action_config.get("gid", None)
+ chmod = action_config.get("chmod", None)
+ recursive = action_config.get("recursive", False)
+ mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
+ is_temporary = False
+
+ vol_type = volume_config.get("type", "")
+ match vol_type:
+ case "temporary":
+ # If it is a temporary volume, we force auto permissions
+ # and set is_temporary to True, so it will be cleaned up
+ is_temporary = True
+ recursive = True
+ case "volume":
+ if not volume_config.get("volume_config", {}).get("auto_permissions", False):
+ return None
+ case "host_path":
+ host_path_config = volume_config.get("host_path_config", {})
+ # Skip when ACL enabled
+ if host_path_config.get("acl_enable", False):
+ return None
+ if not host_path_config.get("auto_permissions", False):
+ return None
+ case "ix_volume":
+ ix_vol_config = volume_config.get("ix_volume_config", {})
+ # Skip when ACL enabled
+ if ix_vol_config.get("acl_enable", False):
+ return None
+ # For ix_volumes, we default to auto_permissions = True
+ if not ix_vol_config.get("auto_permissions", True):
+ return None
+ case _:
+ # Skip for other types
+ return None
+
+ if mode not in valid_modes:
+ raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
+ if not isinstance(uid, int) or not isinstance(gid, int):
+ raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
+ if chmod is not None:
+ chmod = valid_octal_mode_or_raise(chmod)
+
+ mount_path = valid_fs_path_or_raise(mount_path)
+ return {
+ "mount_path": mount_path,
+ "volume_config": volume_config,
+ "action_data": {
+ "mount_path": mount_path,
+ "is_temporary": is_temporary,
+ "identifier": identifier,
+ "recursive": recursive,
+ "mode": mode,
+ "uid": uid,
+ "gid": gid,
+ "chmod": chmod,
+ },
+ }
+
+ def normalize_identifier_for_path(self, identifier: str):
+ return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
+
+ def has_actions(self):
+ return bool(self.actions)
+
+ def activate(self):
+ if len(self.parsed_configs) != len(self.actions):
+ raise RenderError("Number of actions and parsed configs does not match")
+
+ if not self.has_actions():
+ raise RenderError("No actions added. Check if there are actions before activating")
+
+ # Add the container and set it up
+ c = self._render_instance.add_container(self._name, "python_permissions_image")
+ c.set_user(0, 0)
+ c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
+ c.set_network_mode("none")
+
+ # Don't attach any devices
+ c.remove_devices()
+
+ c.deploy.resources.set_profile("medium")
+ c.restart.set_policy("on-failure", maximum_retry_count=1)
+ c.healthcheck.disable()
+
+ c.set_entrypoint(["python3", "/script/run.py"])
+ script = "#!/usr/bin/env python3\n"
+ script += get_script()
+ c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
+
+ actions_data: list[dict] = []
+ for parsed in self.parsed_configs:
+ c.add_storage(parsed["mount_path"], parsed["volume_config"])
+ actions_data.append(parsed["action_data"])
+
+ actions_data_json = json.dumps(actions_data)
+ c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
+
+
+def get_script():
+ return """
+import os
+import json
+import time
+import shutil
+
+with open("/script/actions.json", "r") as f:
+ actions_data = json.load(f)
+
+if not actions_data:
+ # If this script is called, there should be actions data
+ raise ValueError("No actions data found")
+
+def fix_perms(path, chmod, recursive=False):
+ print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
+ os.chmod(path, int(chmod, 8))
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chmod(os.path.join(root, f), int(chmod, 8))
+ print("Permissions after changes:")
+ print_chmod_stat()
+
+def fix_owner(path, uid, gid, recursive=False):
+ print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
+ os.chown(path, uid, gid)
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chown(os.path.join(root, f), uid, gid)
+ print("Ownership after changes:")
+ print_chown_stat()
+
+def print_chown_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
+
+def print_chmod_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
+
+def print_chown_diff(curr_stat, uid, gid):
+ print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
+
+def print_chmod_diff(curr_stat, mode):
+ print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
+
+def perform_action(action):
+ start_time = time.time()
+ print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
+
+ if not os.path.isdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not a directory, skipping...")
+ return
+
+ if action["is_temporary"]:
+ print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
+ for item in os.listdir(action["mount_path"]):
+ item_path = os.path.join(action["mount_path"], item)
+
+ # Exclude the safe directory, where we can use to mount files temporarily
+ if os.path.basename(item_path) == "ix-safe":
+ continue
+ if os.path.isdir(item_path):
+ shutil.rmtree(item_path)
+ else:
+ os.remove(item_path)
+
+ if not action["is_temporary"] and os.listdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not empty, skipping...")
+ return
+
+ print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
+ curr_stat = os.stat(action["mount_path"])
+ print_chown_diff(curr_stat, action["uid"], action["gid"])
+ print_chmod_diff(curr_stat, action["chmod"])
+ print("---")
+
+ if action["mode"] == "always":
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ return
+
+ elif action["mode"] == "check":
+ if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
+ print("Ownership is incorrect. Fixing...")
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ else:
+ print("Ownership is correct. Skipping...")
+
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ if oct(curr_stat.st_mode)[3:] != action["chmod"]:
+ print("Permissions are incorrect. Fixing...")
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ else:
+ print("Permissions are correct. Skipping...")
+
+ print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
+ print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
+ print()
+
+if __name__ == "__main__":
+ start_time = time.time()
+ for action in actions_data:
+ perform_action(action)
+ print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
+"""
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_postgres.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_postgres.py
new file mode 100644
index 0000000000..c40e900d07
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_postgres.py
@@ -0,0 +1,279 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class PostgresConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+MAX_POSTGRES_VERSION = 17
+
+
+class PostgresContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+ self._data_dir = "/var/lib/postgresql/data"
+ self._upgrade_name = f"{self._name}_upgrade"
+ self._upgrade_container = None
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for postgres")
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+
+ c.set_user(999, 999)
+ c.healthcheck.set_test("postgres")
+ c.remove_devices()
+ c.add_storage(self._data_dir, config["volume"])
+
+ common_variables = {
+ "POSTGRES_USER": config["user"],
+ "POSTGRES_PASSWORD": config["password"],
+ "POSTGRES_DB": config["database"],
+ "POSTGRES_PORT": port,
+ }
+
+ for k, v in common_variables.items():
+ c.environment.add_env(k, v)
+
+ perms_instance.add_or_skip_action(
+ f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ repo = self._get_repo(image)
+ # eg we don't want to handle upgrades of pg_vector at the moment
+ if repo == "postgres":
+ target_major_version = self._get_target_version(image)
+ upg = self._render_instance.add_container(self._upgrade_name, image)
+ upg.build_image(get_build_manifest())
+ upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
+ upg.configs.add("pg_container_upgrade.sh", get_upgrade_script(), "/upgrade.sh", "0755")
+ upg.restart.set_policy("on-failure", 1)
+ upg.set_user(999, 999)
+ upg.healthcheck.disable()
+ upg.remove_devices()
+ upg.add_storage(self._data_dir, config["volume"])
+ for k, v in common_variables.items():
+ upg.environment.add_env(k, v)
+
+ upg.environment.add_env("TARGET_VERSION", target_major_version)
+ upg.environment.add_env("DATA_DIR", self._data_dir)
+
+ self._upgrade_container = upg
+
+ c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
+
+ def add_dependency(self, container_name: str, condition: str):
+ self._container.depends.add_dependency(container_name, condition)
+ if self._upgrade_container:
+ self._upgrade_container.depends.add_dependency(container_name, condition)
+
+ def _get_port(self):
+ return self._config.get("port") or 5432
+
+ def _get_repo(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ repo = images[image].get("repository", "")
+ if not repo:
+ raise RenderError("Could not determine repo")
+ return repo
+
+ def _get_target_version(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ tag = images[image].get("tag", "")
+ tag = str(tag) # Account for tags like 16.6
+ target_major_version = tag.split(".")[0]
+
+ try:
+ target_major_version = int(target_major_version)
+ except ValueError:
+ raise RenderError(f"Could not determine target major version from tag [{tag}]")
+
+ if target_major_version > MAX_POSTGRES_VERSION:
+ raise RenderError(f"Postgres version [{target_major_version}] is not supported")
+
+ return target_major_version
+
+ def get_url(self, variant: str):
+ user = urllib.parse.quote_plus(self._config["user"])
+ password = urllib.parse.quote_plus(self._config["password"])
+ creds = f"{user}:{password}"
+ addr = f"{self._name}:{self._get_port()}"
+ db = self._config["database"]
+
+ match variant:
+ case "postgres":
+ return f"postgres://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql":
+ return f"postgresql://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql_no_creds":
+ return f"postgresql://{addr}/{db}?sslmode=disable"
+ case "host_port":
+ return addr
+ case _:
+ raise RenderError(f"Expected [variant] to be one of [postgres, postgresql], got [{variant}]")
+
+
+def get_build_manifest() -> list[str | None]:
+ return [
+ f"RUN apt-get update && apt-get install -y {' '.join(get_upgrade_packages())}",
+ "WORKDIR /tmp",
+ ]
+
+
+def get_upgrade_packages():
+ return [
+ "rsync",
+ "postgresql-13",
+ "postgresql-14",
+ "postgresql-15",
+ "postgresql-16",
+ ]
+
+
+def get_upgrade_script():
+ return """
+#!/bin/bash
+set -euo pipefail
+
+get_bin_path() {
+ local version=$1
+ echo "/usr/lib/postgresql/$version/bin"
+}
+
+log() {
+ echo "[ix-postgres-upgrade] - [$(date +'%Y-%m-%d %H:%M:%S')] - $1"
+}
+
+check_writable() {
+ local path=$1
+ if [ ! -w "$path" ]; then
+ log "$path is not writable"
+ exit 1
+ fi
+}
+
+check_writable "$DATA_DIR"
+
+# Don't do anything if its a fresh install.
+if [ ! -f "$DATA_DIR/PG_VERSION" ]; then
+ log "File $DATA_DIR/PG_VERSION does not exist. Assuming this is a fresh install."
+ exit 0
+fi
+
+# Don't do anything if we're already at the target version.
+OLD_VERSION=$(cat "$DATA_DIR/PG_VERSION")
+log "Current version: $OLD_VERSION"
+log "Target version: $TARGET_VERSION"
+if [ "$OLD_VERSION" -eq "$TARGET_VERSION" ]; then
+ log "Already at target version $TARGET_VERSION"
+ exit 0
+fi
+
+# Fail if we're downgrading.
+if [ "$OLD_VERSION" -gt "$TARGET_VERSION" ]; then
+ log "Cannot downgrade from $OLD_VERSION to $TARGET_VERSION"
+ exit 1
+fi
+
+export OLD_PG_BINARY=$(get_bin_path "$OLD_VERSION")
+if [ ! -f "$OLD_PG_BINARY/pg_upgrade" ]; then
+ log "File $OLD_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_PG_BINARY=$(get_bin_path "$TARGET_VERSION")
+if [ ! -f "$NEW_PG_BINARY/pg_upgrade" ]; then
+ log "File $NEW_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_DATA_DIR="/tmp/new-data-dir"
+if [ -d "$NEW_DATA_DIR" ]; then
+ log "Directory $NEW_DATA_DIR already exists."
+ exit 1
+fi
+
+export PGUSER="$POSTGRES_USER"
+log "Creating new data dir and initializing..."
+PGDATA="$NEW_DATA_DIR" eval "initdb --username=$POSTGRES_USER --pwfile=<(echo $POSTGRES_PASSWORD)"
+
+timestamp=$(date +%Y%m%d%H%M%S)
+backup_name="backup-$timestamp-$OLD_VERSION-$TARGET_VERSION.tar.gz"
+log "Backing up $DATA_DIR to $NEW_DATA_DIR/$backup_name"
+tar -czf "$NEW_DATA_DIR/$backup_name" "$DATA_DIR"
+
+log "Using old pg_upgrade [$OLD_PG_BINARY/pg_upgrade]"
+log "Using new pg_upgrade [$NEW_PG_BINARY/pg_upgrade]"
+log "Checking upgrade compatibility of $OLD_VERSION to $TARGET_VERSION..."
+
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql \
+ --check
+
+log "Compatibility check passed."
+
+log "Upgrading from $OLD_VERSION to $TARGET_VERSION..."
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql
+
+log "Upgrade complete."
+
+log "Copying old pg_hba.conf to new pg_hba.conf"
+# We need to carry this over otherwise
+cp "$DATA_DIR/pg_hba.conf" "$NEW_DATA_DIR/pg_hba.conf"
+
+log "Replacing contents of $DATA_DIR with contents of $NEW_DATA_DIR (including the backup)."
+rsync --archive --delete "$NEW_DATA_DIR/" "$DATA_DIR/"
+
+log "Removing $NEW_DATA_DIR."
+rm -rf "$NEW_DATA_DIR"
+
+log "Done."
+"""
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_redis.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_redis.py
new file mode 100644
index 0000000000..d49ebe7683
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/deps_redis.py
@@ -0,0 +1,71 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise, valid_redis_password_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise, valid_redis_password_or_raise
+
+
+class RedisConfig(TypedDict):
+ password: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+class RedisContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: RedisConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+
+ for key in ("password", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for redis")
+
+ valid_redis_password_or_raise(config["password"])
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(1001, 0)
+ c.healthcheck.set_test("redis")
+ c.remove_devices()
+
+ c.add_storage("/bitnami/redis/data", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_redis_data", config["volume"], {"uid": 1001, "gid": 0, "mode": "check"}
+ )
+
+ c.environment.add_env("ALLOW_EMPTY_PASSWORD", "no")
+ c.environment.add_env("REDIS_PASSWORD", config["password"])
+ c.environment.add_env("REDIS_PORT_NUMBER", port)
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ def _get_port(self):
+ return self._config.get("port") or 6379
+
+ def get_url(self, variant: str):
+ addr = f"{self._name}:{self._get_port()}"
+ password = urllib.parse.quote_plus(self._config["password"])
+
+ match variant:
+ case "redis":
+ return f"redis://default:{password}@{addr}"
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device.py
new file mode 100644
index 0000000000..bfe97097cb
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device.py
@@ -0,0 +1,31 @@
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+
+
+class Device:
+ def __init__(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ hd = valid_fs_path_or_raise(host_device.rstrip("/"))
+ cd = valid_fs_path_or_raise(container_device.rstrip("/"))
+ if not hd or not cd:
+ raise RenderError(
+ "Expected [host_device] and [container_device] to be set. "
+ f"Got host_device [{host_device}] and container_device [{container_device}]"
+ )
+
+ cgroup_perm = valid_cgroup_perm_or_raise(cgroup_perm)
+ if not allow_disallowed:
+ hd = allowed_device_or_raise(hd)
+
+ self.cgroup_perm: str = cgroup_perm
+ self.host_device: str = hd
+ self.container_device: str = cd
+
+ def render(self):
+ result = f"{self.host_device}:{self.container_device}"
+ if self.cgroup_perm:
+ result += f":{self.cgroup_perm}"
+ return result
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py
new file mode 100644
index 0000000000..dcccfee773
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/device_cgroup_rules.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+ def __init__(self, rule: str):
+ rule = valid_device_cgroup_rule_or_raise(rule)
+ parts = rule.split(" ")
+ major, minor = parts[1].split(":")
+
+ self._type = parts[0]
+ self._major = major
+ self._minor = minor
+ self._permissions = parts[2]
+
+ def get_key(self):
+ return f"{self._type}_{self._major}_{self._minor}"
+
+ def render(self):
+ return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._rules: set[DeviceCGroupRule] = set()
+ self._track_rule_combos: set[str] = set()
+
+ def add_rule(self, rule: str):
+ dev_group_rule = DeviceCGroupRule(rule)
+ if dev_group_rule in self._rules:
+ raise RenderError(f"Device Group Rule [{rule}] already added")
+
+ rule_key = dev_group_rule.get_key()
+ if rule_key in self._track_rule_combos:
+ raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+ self._rules.add(dev_group_rule)
+ self._track_rule_combos.add(rule_key)
+
+ def has_rules(self):
+ return len(self._rules) > 0
+
+ def render(self):
+ return sorted([rule.render() for rule in self._rules])
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/devices.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/devices.py
new file mode 100644
index 0000000000..168e98d032
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/devices.py
@@ -0,0 +1,71 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .device import Device
+except ImportError:
+ from error import RenderError
+ from device import Device
+
+
+class Devices:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._devices: set[Device] = set()
+
+ # Tracks all container device paths to make sure they are not duplicated
+ self._container_device_paths: set[str] = set()
+ # Scan values for devices we should automatically add
+ # for example /dev/dri for gpus
+ self._auto_add_devices_from_values()
+
+ def _auto_add_devices_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+
+ if resources.get("gpus", {}).get("use_all_gpus", False):
+ self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
+ if resources["gpus"].get("kfd_device_exists", False):
+ self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
+
+ def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ # Host device can be mapped to multiple container devices,
+ # so we only make sure container devices are not duplicated
+ if container_device in self._container_device_paths:
+ raise RenderError(f"Device with container path [{container_device}] already added")
+
+ self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
+ self._container_device_paths.add(container_device)
+
+ def add_usb_bus(self):
+ self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
+
+ def _add_snd_device(self):
+ self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
+
+ def _add_tun_device(self):
+ self.add_device("/dev/net/tun", "/dev/net/tun", allow_disallowed=True)
+
+ def has_devices(self):
+ return len(self._devices) > 0
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._devices.clear()
+ self._container_device_paths.clear()
+
+ # Check if there are any gpu devices
+ # Used to determine if we should add groups
+ # like 'video' to the container
+ def has_gpus(self):
+ for d in self._devices:
+ if d.host_device == "/dev/dri":
+ return True
+ return False
+
+ def render(self) -> list[str]:
+ return sorted([d.render() for d in self._devices])
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/dns.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/dns.py
new file mode 100644
index 0000000000..d3ae7b19fa
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/dns.py
@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import allowed_dns_opt_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import allowed_dns_opt_or_raise
+
+
+class Dns:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dns_options: set[str] = set()
+ self._dns_searches: set[str] = set()
+ self._dns_nameservers: set[str] = set()
+
+ self._auto_add_dns_opts_from_values()
+ self._auto_add_dns_searches_from_values()
+ self._auto_add_dns_nameservers_from_values()
+
+ def _get_dns_opt_keys(self):
+ return [self._get_key_from_opt(opt) for opt in self._dns_options]
+
+ def _get_key_from_opt(self, opt):
+ return opt.split(":")[0]
+
+ def _auto_add_dns_opts_from_values(self):
+ values = self._render_instance.values
+ for dns_opt in values.get("network", {}).get("dns_opts", []):
+ self.add_dns_opt(dns_opt)
+
+ def _auto_add_dns_searches_from_values(self):
+ values = self._render_instance.values
+ for dns_search in values.get("network", {}).get("dns_searches", []):
+ self.add_dns_search(dns_search)
+
+ def _auto_add_dns_nameservers_from_values(self):
+ values = self._render_instance.values
+ for dns_nameserver in values.get("network", {}).get("dns_nameservers", []):
+ self.add_dns_nameserver(dns_nameserver)
+
+ def add_dns_search(self, dns_search):
+ if dns_search in self._dns_searches:
+ raise RenderError(f"DNS Search [{dns_search}] already added")
+ self._dns_searches.add(dns_search)
+
+ def add_dns_nameserver(self, dns_nameserver):
+ if dns_nameserver in self._dns_nameservers:
+ raise RenderError(f"DNS Nameserver [{dns_nameserver}] already added")
+ self._dns_nameservers.add(dns_nameserver)
+
+ def add_dns_opt(self, dns_opt):
+ # eg attempts:3
+ key = allowed_dns_opt_or_raise(self._get_key_from_opt(dns_opt))
+ if key in self._get_dns_opt_keys():
+ raise RenderError(f"DNS Option [{key}] already added")
+ self._dns_options.add(dns_opt)
+
+ def has_dns_opts(self):
+ return len(self._dns_options) > 0
+
+ def has_dns_searches(self):
+ return len(self._dns_searches) > 0
+
+ def has_dns_nameservers(self):
+ return len(self._dns_nameservers) > 0
+
+ def render_dns_searches(self):
+ return sorted(self._dns_searches)
+
+ def render_dns_opts(self):
+ return sorted(self._dns_options)
+
+ def render_dns_nameservers(self):
+ return sorted(self._dns_nameservers)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/environment.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/environment.py
new file mode 100644
index 0000000000..056763ea80
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/environment.py
@@ -0,0 +1,112 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .resources import Resources
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from resources import Resources
+
+
+class Environment:
+ def __init__(self, render_instance: "Render", resources: Resources):
+ self._render_instance = render_instance
+ self._resources = resources
+ # Stores variables that user defined
+ self._user_vars: dict[str, Any] = {}
+ # Stores variables that are automatically added (based on values)
+ self._auto_variables: dict[str, Any] = {}
+ # Stores variables that are added by the application developer
+ self._app_dev_variables: dict[str, Any] = {}
+
+ self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
+
+ self._auto_add_variables_from_values()
+
+ def _auto_add_variables_from_values(self):
+ if not self._skip_generic_variables:
+ self._add_generic_variables()
+ self._add_nvidia_variables()
+
+ def _add_generic_variables(self):
+ self._auto_variables["TZ"] = self._render_instance.values.get("TZ", "Etc/UTC")
+ self._auto_variables["UMASK"] = self._render_instance.values.get("UMASK", "002")
+ self._auto_variables["UMASK_SET"] = self._render_instance.values.get("UMASK", "002")
+
+ run_as = self._render_instance.values.get("run_as", {})
+ user = run_as.get("user")
+ group = run_as.get("group")
+ if user:
+ self._auto_variables["PUID"] = user
+ self._auto_variables["UID"] = user
+ self._auto_variables["USER_ID"] = user
+ if group:
+ self._auto_variables["PGID"] = group
+ self._auto_variables["GID"] = group
+ self._auto_variables["GROUP_ID"] = group
+
+ def _add_nvidia_variables(self):
+ if self._resources._nvidia_ids:
+ self._auto_variables["NVIDIA_DRIVER_CAPABILITIES"] = "all"
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(sorted(self._resources._nvidia_ids))
+ else:
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = "void"
+
+ def _format_value(self, v: Any) -> str:
+ value = str(v)
+
+ # str(bool) returns "True" or "False",
+ # but we want "true" or "false"
+ if isinstance(v, bool):
+ value = value.lower()
+ return value
+
+ def add_env(self, name: str, value: Any):
+ if not name:
+ raise RenderError(f"Environment variable name cannot be empty. [{name}]")
+ if name in self._app_dev_variables.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{name}] in application developer environment variables."
+ )
+ self._app_dev_variables[name] = value
+
+ def add_user_envs(self, user_env: list[dict]):
+ for item in user_env:
+ if not item.get("name"):
+ raise RenderError(f"Environment variable name cannot be empty. [{item}]")
+ if item["name"] in self._user_vars.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{item['name']}] in user environment variables."
+ )
+ self._user_vars[item["name"]] = item.get("value")
+
+ def has_variables(self):
+ return len(self._auto_variables) > 0 or len(self._user_vars) > 0 or len(self._app_dev_variables) > 0
+
+ def render(self):
+ result: dict[str, str] = {}
+
+ # Add envs from auto variables
+ result.update({k: self._format_value(v) for k, v in self._auto_variables.items()})
+
+ # Track defined keys for faster lookup
+ defined_keys = set(result.keys())
+
+ # Add envs from application developer (prohibit overwriting auto variables)
+ for k, v in self._app_dev_variables.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined automatically from the library.")
+ result[k] = self._format_value(v)
+ defined_keys.add(k)
+
+ # Add envs from user (prohibit overwriting app developer envs and auto variables)
+ for k, v in self._user_vars.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined from the application developer.")
+ result[k] = self._format_value(v)
+
+ return {k: escape_dollar(v) for k, v in result.items()}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/error.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/error.py
new file mode 100644
index 0000000000..aef48d3b02
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/error.py
@@ -0,0 +1,4 @@
+class RenderError(Exception):
+ """Base class for exceptions in this module."""
+
+ pass
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/expose.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/expose.py
new file mode 100644
index 0000000000..a3ac0aec59
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/expose.py
@@ -0,0 +1,31 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_port_or_raise, valid_port_protocol_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_port_or_raise, valid_port_protocol_or_raise
+
+
+class Expose:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: set[str] = set()
+
+ def add_port(self, port: int, protocol: str = "tcp"):
+ port = valid_port_or_raise(port)
+ protocol = valid_port_protocol_or_raise(protocol)
+ key = f"{port}/{protocol}"
+ if key in self._ports:
+ raise RenderError(f"Exposed port [{port}/{protocol}] already added")
+ self._ports.add(key)
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ return sorted(self._ports)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/extra_hosts.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/extra_hosts.py
new file mode 100644
index 0000000000..eaad3bed26
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/extra_hosts.py
@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+
+class ExtraHosts:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._extra_hosts: dict[str, str] = {}
+
+ def add_host(self, host: str, ip: str):
+ if not ip == "host-gateway":
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+ if host in self._extra_hosts:
+ raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+ self._extra_hosts[host] = ip
+
+ def has_hosts(self):
+ return len(self._extra_hosts) > 0
+
+ def render(self):
+ return {host: ip for host, ip in self._extra_hosts.items()}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/formatter.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/formatter.py
new file mode 100644
index 0000000000..24e882f47a
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/formatter.py
@@ -0,0 +1,26 @@
+import json
+import hashlib
+
+
+def escape_dollar(text: str) -> str:
+ return text.replace("$", "$$")
+
+
+def get_hashed_name_for_volume(prefix: str, config: dict):
+ config_hash = hashlib.sha256(json.dumps(config).encode("utf-8")).hexdigest()
+ return f"{prefix}_{config_hash}"
+
+
+def get_hash_with_prefix(prefix: str, data: str):
+ return f"{prefix}_{hashlib.sha256(data.encode('utf-8')).hexdigest()}"
+
+
+def merge_dicts_no_overwrite(dict1, dict2):
+ overlapping_keys = dict1.keys() & dict2.keys()
+ if overlapping_keys:
+ raise ValueError(f"Merging of dicts failed. Overlapping keys: {overlapping_keys}")
+ return {**dict1, **dict2}
+
+
+def get_image_with_hashed_data(image: str, data: str):
+ return get_hash_with_prefix(f"ix-{image}", data)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/functions.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/functions.py
new file mode 100644
index 0000000000..02c9cd4708
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/functions.py
@@ -0,0 +1,149 @@
+import re
+import copy
+import bcrypt
+import secrets
+from base64 import b64encode
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .volume_sources import HostPathSource, IxVolumeSource
+except ImportError:
+ from error import RenderError
+ from volume_sources import HostPathSource, IxVolumeSource
+
+
+class Functions:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def _bcrypt_hash(self, password):
+ hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+ return hashed
+
+ def _htpasswd(self, username, password):
+ hashed = self._bcrypt_hash(password)
+ return username + ":" + hashed
+
+ def _secure_string(self, length):
+ return secrets.token_urlsafe(length)[:length]
+
+ def _basic_auth(self, username, password):
+ return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
+
+ def _basic_auth_header(self, username, password):
+ return f"Basic {self._basic_auth(username, password)}"
+
+ def _fail(self, message):
+ raise RenderError(message)
+
+ def _camel_case(self, string):
+ return string.title()
+
+ def _auto_cast(self, value):
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return float(value)
+ except ValueError:
+ pass
+
+ if value.lower() in ["true", "false"]:
+ return value.lower() == "true"
+
+ return value
+
+ def _match_regex(self, value, regex):
+ if not re.match(regex, value):
+ return False
+ return True
+
+ def _must_match_regex(self, value, regex):
+ if not self._match_regex(value, regex):
+ raise RenderError(f"Expected [{value}] to match [{regex}]")
+ return value
+
+ def _is_boolean(self, string):
+ return string.lower() in ["true", "false"]
+
+ def _is_number(self, string):
+ try:
+ float(string)
+ return True
+ except ValueError:
+ return False
+
+ def _copy_dict(self, dict):
+ return copy.deepcopy(dict)
+
+ def _merge_dicts(self, *dicts):
+ merged_dict = {}
+ for dictionary in dicts:
+ merged_dict.update(dictionary)
+ return merged_dict
+
+ def _disallow_chars(self, string: str, chars: list[str], key: str):
+ for char in chars:
+ if char in string:
+ raise RenderError(f"Disallowed character [{char}] in [{key}]")
+ return string
+
+ def _or_default(self, value, default):
+ if not value:
+ return default
+ return value
+
+ def _temp_config(self, name):
+ if not name:
+ raise RenderError("Expected [name] to be set when calling [temp_config].")
+ return {"type": "temporary", "volume_config": {"volume_name": name}}
+
+ def _get_host_path(self, storage):
+ source_type = storage.get("type", "")
+ if not source_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match source_type:
+ case "host_path":
+ mount_config = storage.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ host_source = HostPathSource(self._render_instance, mount_config).get()
+ return host_source
+ case "ix_volume":
+ mount_config = storage.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ ix_source = IxVolumeSource(self._render_instance, mount_config).get()
+ return ix_source
+ case _:
+ raise RenderError(f"Storage type [{source_type}] does not support host path.")
+
+ def func_map(self):
+ # TODO: Check what is no longer used and remove
+ return {
+ "auto_cast": self._auto_cast,
+ "basic_auth_header": self._basic_auth_header,
+ "basic_auth": self._basic_auth,
+ "bcrypt_hash": self._bcrypt_hash,
+ "camel_case": self._camel_case,
+ "copy_dict": self._copy_dict,
+ "fail": self._fail,
+ "htpasswd": self._htpasswd,
+ "is_boolean": self._is_boolean,
+ "is_number": self._is_number,
+ "match_regex": self._match_regex,
+ "merge_dicts": self._merge_dicts,
+ "must_match_regex": self._must_match_regex,
+ "secure_string": self._secure_string,
+ "disallow_chars": self._disallow_chars,
+ "get_host_path": self._get_host_path,
+ "or_default": self._or_default,
+ "temp_config": self._temp_config,
+ }
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/healthcheck.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/healthcheck.py
new file mode 100644
index 0000000000..0805329284
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/healthcheck.py
@@ -0,0 +1,203 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_http_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_http_path_or_raise
+
+
+class Healthcheck:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._test: str | list[str] = ""
+ self._interval_sec: int = 10
+ self._timeout_sec: int = 5
+ self._retries: int = 30
+ self._start_period_sec: int = 10
+ self._disabled: bool = False
+ self._use_built_in: bool = False
+
+ def _get_test(self):
+ if isinstance(self._test, str):
+ return escape_dollar(self._test)
+
+ return [escape_dollar(t) for t in self._test]
+
+ def disable(self):
+ self._disabled = True
+
+ def use_built_in(self):
+ self._use_built_in = True
+
+ def set_custom_test(self, test: str | list[str]):
+ if self._disabled:
+ raise RenderError("Cannot set custom test when healthcheck is disabled")
+ self._test = test
+
+ def set_test(self, variant: str, config: dict | None = None):
+ config = config or {}
+ self.set_custom_test(test_mapping(variant, config))
+
+ def set_interval(self, interval: int):
+ self._interval_sec = interval
+
+ def set_timeout(self, timeout: int):
+ self._timeout_sec = timeout
+
+ def set_retries(self, retries: int):
+ self._retries = retries
+
+ def set_start_period(self, start_period: int):
+ self._start_period_sec = start_period
+
+ def has_healthcheck(self):
+ return not self._use_built_in
+
+ def render(self):
+ if self._use_built_in:
+ return RenderError("Should not be called when built in healthcheck is used")
+
+ if self._disabled:
+ return {"disable": True}
+
+ if not self._test:
+ raise RenderError("Healthcheck test is not set")
+
+ return {
+ "test": self._get_test(),
+ "interval": f"{self._interval_sec}s",
+ "timeout": f"{self._timeout_sec}s",
+ "retries": self._retries,
+ "start_period": f"{self._start_period_sec}s",
+ }
+
+
+def test_mapping(variant: str, config: dict | None = None) -> str:
+ config = config or {}
+ tests = {
+ "curl": curl_test,
+ "wget": wget_test,
+ "http": http_test,
+ "netcat": netcat_test,
+ "tcp": tcp_test,
+ "redis": redis_test,
+ "postgres": postgres_test,
+ "mariadb": mariadb_test,
+ }
+
+ if variant not in tests:
+ raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
+
+ return tests[variant](config)
+
+
+def get_key(config: dict, key: str, default: Any, required: bool):
+ if not config.get(key):
+ if not required:
+ return default
+ raise RenderError(f"Expected [{key}] to be set")
+ return config[key]
+
+
+def curl_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--insecure")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for curl test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "curl --silent --output /dev/null --show-error --fail"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def wget_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--no-check-certificate")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for wget test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "wget --spider --quiet"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def http_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""" # noqa
+
+
+def netcat_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"nc -z -w 1 {host} {port}"
+
+
+def tcp_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"timeout 1 bash -c 'cat < /dev/null > /dev/tcp/{host}/{port}'"
+
+
+def redis_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 6379, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"redis-cli -h {host} -p {port} -a $REDIS_PASSWORD ping | grep -q PONG"
+
+
+def postgres_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 5432, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"pg_isready -h {host} -p {port} -U $POSTGRES_USER -d $POSTGRES_DB"
+
+
+def mariadb_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 3306, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"mariadb-admin --user=root --host={host} --port={port} --password=$MARIADB_ROOT_PASSWORD ping"
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/labels.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/labels.py
new file mode 100644
index 0000000000..f1e667ba00
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/labels.py
@@ -0,0 +1,37 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+
+
+class Labels:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._labels: dict[str, str] = {}
+
+ def add_label(self, key: str, value: str):
+ if not key:
+ raise RenderError("Labels must have a key")
+
+ if key.startswith("com.docker.compose"):
+ raise RenderError(f"Label [{key}] cannot start with [com.docker.compose] as it is reserved")
+
+ if key in self._labels.keys():
+ raise RenderError(f"Label [{key}] already added")
+
+ self._labels[key] = escape_dollar(str(value))
+
+ def has_labels(self) -> bool:
+ return bool(self._labels)
+
+ def render(self) -> dict[str, str]:
+ if not self.has_labels():
+ return {}
+ return {label: value for label, value in sorted(self._labels.items())}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/notes.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/notes.py
new file mode 100644
index 0000000000..eb8d9f37fc
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/notes.py
@@ -0,0 +1,76 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+class Notes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._app_name: str = ""
+ self._app_train: str = ""
+ self._warnings: list[str] = []
+ self._deprecations: list[str] = []
+ self._header: str = ""
+ self._body: str = ""
+ self._footer: str = ""
+
+ self._auto_set_app_name()
+ self._auto_set_app_train()
+ self._auto_set_header()
+ self._auto_set_footer()
+
+ def _is_enterprise_train(self):
+ if self._app_train == "enterprise":
+ return True
+
+ def _auto_set_app_name(self):
+ app_name = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("title", "")
+ self._app_name = app_name or ""
+
+ def _auto_set_app_train(self):
+ app_train = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("train", "")
+ self._app_train = app_train or ""
+
+ def _auto_set_header(self):
+ self._header = f"# {self._app_name}\n\n"
+
+ def _auto_set_footer(self):
+ url = "https://github.com/truenas/apps"
+ if self._is_enterprise_train():
+ url = "https://ixsystems.atlassian.net"
+ footer = "## Bug Reports and Feature Requests\n\n"
+ footer += "If you find a bug in this app or have an idea for a new feature, please file an issue at\n"
+ footer += f"{url}\n\n"
+ self._footer = footer
+
+ def add_warning(self, warning: str):
+ self._warnings.append(warning)
+
+ def add_deprecation(self, deprecation: str):
+ self._deprecations.append(deprecation)
+
+ def set_body(self, body: str):
+ self._body = body
+
+ def render(self):
+ result = self._header
+
+ if self._warnings:
+ result += "## Warnings\n\n"
+ for warning in self._warnings:
+ result += f"- {warning}\n"
+ result += "\n"
+
+ if self._deprecations:
+ result += "## Deprecations\n\n"
+ for deprecation in self._deprecations:
+ result += f"- {deprecation}\n"
+ result += "\n"
+
+ if self._body:
+ result += self._body.strip() + "\n\n"
+
+ result += self._footer
+
+ return result
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portal.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portal.py
new file mode 100644
index 0000000000..cf47163439
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portal.py
@@ -0,0 +1,22 @@
+try:
+ from .validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+except ImportError:
+ from validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+
+
+class Portal:
+ def __init__(self, name: str, config: dict):
+ self._name = name
+ self._scheme = valid_portal_scheme_or_raise(config.get("scheme", "http"))
+ self._host = config.get("host", "0.0.0.0") or "0.0.0.0"
+ self._port = valid_port_or_raise(config.get("port", 0))
+ self._path = valid_http_path_or_raise(config.get("path", "/"))
+
+ def render(self):
+ return {
+ "name": self._name,
+ "scheme": self._scheme,
+ "host": self._host,
+ "port": self._port,
+ "path": self._path,
+ }
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portals.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portals.py
new file mode 100644
index 0000000000..e106d231e6
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/portals.py
@@ -0,0 +1,28 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .portal import Portal
+except ImportError:
+ from error import RenderError
+ from portal import Portal
+
+
+class Portals:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._portals: set[Portal] = set()
+
+ def add_portal(self, config: dict):
+ name = config.get("name", "Web UI")
+
+ if name in [p._name for p in self._portals]:
+ raise RenderError(f"Portal [{name}] already added")
+
+ self._portals.add(Portal(name, config))
+
+ def render(self):
+ return [p.render() for _, p in sorted([(p._name, p) for p in self._portals])]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/ports.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/ports.py
new file mode 100644
index 0000000000..e571232ac9
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/ports.py
@@ -0,0 +1,153 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+except ImportError:
+ from error import RenderError
+ from validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+
+
+class Ports:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: dict[str, dict] = {}
+
+ def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str:
+ return f"{host_port}_{host_ip}_{proto}_{ip_family}"
+
+ def _is_wildcard_ip(self, ip: str) -> bool:
+ return ip in ["0.0.0.0", "::"]
+
+ def _get_opposite_wildcard(self, ip: str) -> str:
+ return "0.0.0.0" if ip == "::" else "::"
+
+ def _get_sort_key(self, p: dict) -> str:
+ return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}"
+
+ def _is_ports_same(self, port1: dict, port2: dict) -> bool:
+ return (
+ port1["published"] == port2["published"]
+ and port1["target"] == port2["target"]
+ and port1["protocol"] == port2["protocol"]
+ and port1.get("host_ip", "_") == port2.get("host_ip", "_")
+ )
+
+ def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool:
+ comparison_port = port_config.copy()
+ comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"])
+ for p in wildcard_ports.values():
+ if self._is_ports_same(comparison_port, p):
+ return True
+ return False
+
+ def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None:
+ host_port = port_config["published"]
+ host_ip = port_config["host_ip"]
+ proto = port_config["protocol"]
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip_family)
+
+ if key in self._ports.keys():
+ raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]")
+
+ wildcard_ip = "0.0.0.0" if ip_family == 4 else "::"
+ if host_ip != wildcard_ip:
+ # Check if there is a port with same details but with wildcard IP of the same family
+ wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family)
+ if wildcard_key in self._ports.keys():
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{wildcard_ip}]"
+ )
+ else:
+ # We are adding a port with wildcard IP
+ # Check if there is a port with same details but with specific IP of the same family
+ for p in self._ports.values():
+ # Skip if the port is not for the same family
+ if ip_family != ipaddress.ip_address(p["host_ip"]).version:
+ continue
+
+ # Make a copy of the port config
+ search_port = p.copy()
+ # Replace the host IP with wildcard IP
+ search_port["host_ip"] = wildcard_ip
+ # If the ports match, means that a port for specific IP is already added
+ # and we are trying to add it again with wildcard IP. Raise an error
+ if self._is_ports_same(search_port, port_config):
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{p['host_ip']}]"
+ )
+
+ def add_port(self, host_port: int, container_port: int, config: dict | None = None):
+ config = config or {}
+ host_port = valid_port_or_raise(host_port)
+ container_port = valid_port_or_raise(container_port)
+ proto = valid_port_protocol_or_raise(config.get("protocol", "tcp"))
+ mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+
+ # TODO: Once all apps stop using this function directly, (ie using the container.add_port function)
+ # Remove this, and let container.add_port call this for each host_ip
+ host_ip = config.get("host_ip", None)
+ if host_ip is None:
+ self.add_port(host_port, container_port, config | {"host_ip": "0.0.0.0"})
+ self.add_port(host_port, container_port, config | {"host_ip": "::"})
+ return
+
+ host_ip = valid_ip_or_raise(config.get("host_ip", None))
+ ip = ipaddress.ip_address(host_ip)
+
+ port_config = {
+ "published": host_port,
+ "target": container_port,
+ "protocol": proto,
+ "mode": mode,
+ "host_ip": host_ip,
+ }
+ self._check_port_conflicts(port_config, ip.version)
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip.version)
+ self._ports[key] = port_config
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ specific_ports = []
+ wildcard_ports = {}
+
+ for port_config in self._ports.values():
+ if self._is_wildcard_ip(port_config["host_ip"]):
+ wildcard_ports[id(port_config)] = port_config.copy()
+ else:
+ specific_ports.append(port_config.copy())
+
+ processed_ports = specific_ports.copy()
+ for wild_port in wildcard_ports.values():
+ processed_port = wild_port.copy()
+
+ # Check if there's a matching wildcard port for the opposite IP family
+ has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports)
+
+ if has_opposite_family:
+ processed_port.pop("host_ip")
+
+ if processed_port not in processed_ports:
+ processed_ports.append(processed_port)
+
+ return sorted(processed_ports, key=self._get_sort_key)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/render.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/render.py
new file mode 100644
index 0000000000..9d8fcc28d5
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/render.py
@@ -0,0 +1,89 @@
+import copy
+
+try:
+ from .configs import Configs
+ from .container import Container
+ from .deps import Deps
+ from .error import RenderError
+ from .functions import Functions
+ from .notes import Notes
+ from .portals import Portals
+ from .volumes import Volumes
+except ImportError:
+ from configs import Configs
+ from container import Container
+ from deps import Deps
+ from error import RenderError
+ from functions import Functions
+ from notes import Notes
+ from portals import Portals
+ from volumes import Volumes
+
+
+class Render(object):
+ def __init__(self, values):
+ self._containers: dict[str, Container] = {}
+ self.values = values
+ self._add_images_internal_use()
+ # Make a copy after we inject the images
+ self._original_values: dict = copy.deepcopy(self.values)
+
+ self.deps: Deps = Deps(self)
+
+ self.configs = Configs(render_instance=self)
+ self.funcs = Functions(render_instance=self).func_map()
+ self.portals: Portals = Portals(render_instance=self)
+ self.notes: Notes = Notes(render_instance=self)
+ self.volumes = Volumes(render_instance=self)
+
+ def _add_images_internal_use(self):
+ if not self.values.get("images"):
+ self.values["images"] = {}
+
+ if "python_permissions_image" not in self.values["images"]:
+ self.values["images"]["python_permissions_image"] = {"repository": "python", "tag": "3.13.0-slim-bookworm"}
+
+ def container_names(self):
+ return list(self._containers.keys())
+
+ def add_container(self, name: str, image: str):
+ name = name.strip()
+ if not name:
+ raise RenderError("Container name cannot be empty")
+ container = Container(self, name, image)
+ if name in self._containers:
+ raise RenderError(f"Container {name} already exists.")
+ self._containers[name] = container
+ return container
+
+ def render(self):
+ if self.values != self._original_values:
+ raise RenderError("Values have been modified since the renderer was created.")
+
+ if not self._containers:
+ raise RenderError("No containers added.")
+
+ result: dict = {
+ "x-notes": self.notes.render(),
+ "x-portals": self.portals.render(),
+ "services": {c._name: c.render() for c in self._containers.values()},
+ }
+
+ # Make sure that after services are rendered
+ # there are no labels that target a non-existent container
+ # This is to prevent typos
+ for label in self.values.get("labels", []):
+ for c in label.get("containers", []):
+ if c not in self.container_names():
+ raise RenderError(f"Label [{label['key']}] references container [{c}] which does not exist")
+
+ if self.volumes.has_volumes():
+ result["volumes"] = self.volumes.render()
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ # if self.networks:
+ # result["networks"] = {...}
+
+ return result
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/resources.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/resources.py
new file mode 100644
index 0000000000..733f43bb6f
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/resources.py
@@ -0,0 +1,115 @@
+import re
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+DEFAULT_CPUS = 2.0
+DEFAULT_MEMORY = 4096
+
+
+class Resources:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._limits: dict = {}
+ self._reservations: dict = {}
+ self._nvidia_ids: set[str] = set()
+ self._auto_add_cpu_from_values()
+ self._auto_add_memory_from_values()
+ self._auto_add_gpus_from_values()
+
+ def _set_cpu(self, cpus: Any):
+ c = str(cpus)
+ if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", c):
+ raise RenderError(f"Expected cpus to be a number or a float (minimum 1.0), got [{cpus}]")
+ self._limits.update({"cpus": c})
+
+ def _set_memory(self, memory: Any):
+ m = str(memory)
+ if not re.match(r"^[1-9][0-9]*$", m):
+ raise RenderError(f"Expected memory to be a number, got [{memory}]")
+ self._limits.update({"memory": f"{m}M"})
+
+ def _auto_add_cpu_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_cpu(resources.get("limits", {}).get("cpus", DEFAULT_CPUS))
+
+ def _auto_add_memory_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_memory(resources.get("limits", {}).get("memory", DEFAULT_MEMORY))
+
+ def _auto_add_gpus_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ gpus = resources.get("gpus", {}).get("nvidia_gpu_selection", {})
+ if not gpus:
+ return
+
+ for pci, gpu in gpus.items():
+ if gpu.get("use_gpu", False):
+ if not gpu.get("uuid"):
+ raise RenderError(f"Expected [uuid] to be set for GPU in slot [{pci}] in [nvidia_gpu_selection]")
+ self._nvidia_ids.add(gpu["uuid"])
+
+ if self._nvidia_ids:
+ if not self._reservations:
+ self._reservations["devices"] = []
+ self._reservations["devices"].append(
+ {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": sorted(self._nvidia_ids),
+ }
+ )
+
+ # This is only used on ix-app that we allow
+ # disabling cpus and memory. GPUs are only added
+ # if the user has requested them.
+ def remove_cpus_and_memory(self):
+ self._limits.pop("cpus", None)
+ self._limits.pop("memory", None)
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._reservations.pop("devices", None)
+
+ def set_profile(self, profile: str):
+ cpu, memory = profile_mapping(profile)
+ self._set_cpu(cpu)
+ self._set_memory(memory)
+
+ def has_resources(self):
+ return len(self._limits) > 0 or len(self._reservations) > 0
+
+ def has_gpus(self):
+ gpu_devices = [d for d in self._reservations.get("devices", []) if "gpu" in d["capabilities"]]
+ return len(gpu_devices) > 0
+
+ def render(self):
+ result = {}
+ if self._limits:
+ result["limits"] = self._limits
+ if self._reservations:
+ result["reservations"] = self._reservations
+
+ return result
+
+
+def profile_mapping(profile: str):
+ profiles = {
+ "low": (1, 512),
+ "medium": (2, 1024),
+ }
+
+ if profile not in profiles:
+ raise RenderError(
+ f"Resource profile [{profile}] is not valid. Valid options are: [{', '.join(profiles.keys())}]"
+ )
+
+ return profiles[profile]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/restart.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/restart.py
new file mode 100644
index 0000000000..2f6281af48
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/restart.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .validations import valid_restart_policy_or_raise
+except ImportError:
+ from validations import valid_restart_policy_or_raise
+
+
+class RestartPolicy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._policy: str = "unless-stopped"
+ self._maximum_retry_count: int = 0
+
+ def set_policy(self, policy: str, maximum_retry_count: int = 0):
+ self._policy = valid_restart_policy_or_raise(policy, maximum_retry_count)
+ self._maximum_retry_count = maximum_retry_count
+
+ def render(self):
+ if self._policy == "on-failure" and self._maximum_retry_count > 0:
+ return f"{self._policy}:{self._maximum_retry_count}"
+ return self._policy
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/storage.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/storage.py
new file mode 100644
index 0000000000..f1650259b3
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/storage.py
@@ -0,0 +1,116 @@
+from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise
+ from .volume_mount import VolumeMount
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise
+ from volume_mount import VolumeMount
+
+
+class IxStorageTmpfsConfig(TypedDict):
+ size: NotRequired[int]
+ mode: NotRequired[str]
+
+
+class AclConfig(TypedDict, total=False):
+ path: str
+
+
+class IxStorageHostPathConfig(TypedDict):
+ path: NotRequired[str] # Either this or acl.path must be set
+ acl_enable: NotRequired[bool]
+ acl: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageIxVolumeConfig(TypedDict):
+ dataset_name: str
+ acl_enable: NotRequired[bool]
+ acl_entries: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageVolumeConfig(TypedDict):
+ volume_name: NotRequired[str]
+ nocopy: NotRequired[bool]
+ auto_permissions: NotRequired[bool]
+
+
+class IxStorageNfsConfig(TypedDict):
+ server: str
+ path: str
+ options: NotRequired[list[str]]
+
+
+class IxStorageCifsConfig(TypedDict):
+ server: str
+ path: str
+ username: str
+ password: str
+ domain: NotRequired[str]
+ options: NotRequired[list[str]]
+
+
+IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
+IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
+IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
+
+
+class IxStorage(TypedDict):
+ type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
+ read_only: NotRequired[bool]
+
+ ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
+ host_path_config: NotRequired[IxStorageHostPathConfig]
+ tmpfs_config: NotRequired[IxStorageTmpfsConfig]
+ volume_config: NotRequired[IxStorageVolumeConfig]
+ nfs_config: NotRequired[IxStorageNfsConfig]
+ cifs_config: NotRequired[IxStorageCifsConfig]
+
+
+class Storage:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volume_mounts: set[VolumeMount] = set()
+
+ def add(self, mount_path: str, config: "IxStorage"):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ if mount_path in [m.mount_path for m in self._volume_mounts]:
+ raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+
+ volume_mount = VolumeMount(self._render_instance, mount_path, config)
+ self._volume_mounts.add(volume_mount)
+
+ def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def _add_udev(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/run/udev", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def has_mounts(self) -> bool:
+ return bool(self._volume_mounts)
+
+ def render(self):
+ return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/sysctls.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/sysctls.py
new file mode 100644
index 0000000000..e6b8469f3b
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/sysctls.py
@@ -0,0 +1,38 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from container import Container
+
+try:
+ from .error import RenderError
+ from .validations import valid_sysctl_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_sysctl_or_raise
+
+
+class Sysctls:
+ def __init__(self, render_instance: "Render", container_instance: "Container"):
+ self._render_instance = render_instance
+ self._container_instance = container_instance
+ self._sysctls: dict = {}
+
+ def add(self, key: str, value):
+ key = key.strip()
+ if not key:
+ raise RenderError("Sysctls key cannot be empty")
+ if value is None:
+ raise RenderError(f"Sysctl [{key}] requires a value")
+ if key in self._sysctls:
+ raise RenderError(f"Sysctl [{key}] already added")
+ self._sysctls[key] = str(value)
+
+ def has_sysctls(self):
+ return bool(self._sysctls)
+
+ def render(self):
+ if not self.has_sysctls():
+ return {}
+ host_net = self._container_instance._network_mode == "host"
+ return {valid_sysctl_or_raise(k, host_net): v for k, v in self._sysctls.items()}
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/__init__.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py
new file mode 100644
index 0000000000..f30c1210ed
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_build_image.py
@@ -0,0 +1,49 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_build_image_with_from(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.build_image(["FROM test_image"])
+
+
+def test_build_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.build_image(
+ [
+ "RUN echo hello",
+ None,
+ "",
+ "RUN echo world",
+ ]
+ )
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["image"]
+ == "ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"
+ )
+ assert output["services"]["test_container"]["build"] == {
+ "tags": ["ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"],
+ "dockerfile_inline": """FROM nginx:latest
+RUN echo hello
+RUN echo world
+""",
+ }
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py
new file mode 100644
index 0000000000..9049e473ea
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_configs.py
@@ -0,0 +1,63 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_duplicate_config_with_different_data(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data2", "/some/path")
+
+
+def test_add_config_with_empty_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data", "")
+
+
+def test_add_duplicate_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config2", "test_data2", "/some/path")
+
+
+def test_add_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "$test_data", "/some/path")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "$$test_data"
+ assert output["services"]["test_container"]["configs"] == [{"source": "test_config", "target": "/some/path"}]
+
+
+def test_add_config_with_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path", "0777")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "test_data"
+ assert output["services"]["test_container"]["configs"] == [
+ {"source": "test_config", "target": "/some/path", "mode": 511}
+ ]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_container.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_container.py
new file mode 100644
index 0000000000..1980dcd5df
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_container.py
@@ -0,0 +1,425 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_empty_container_name(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container(" ", "test_image")
+
+
+def test_resolve_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["image"] == "nginx:latest"
+
+
+def test_missing_repo(mock_values):
+ mock_values["images"]["test_image"]["repository"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_missing_tag(mock_values):
+ mock_values["images"]["test_image"]["tag"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_non_existing_image(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "non_existing_image")
+
+
+def test_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_pull_policy("always")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["pull_policy"] == "always"
+
+
+def test_invalid_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.set_pull_policy("invalid_policy")
+
+
+def test_clear_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.add_caps(["NET_ADMIN"])
+ c1.clear_caps()
+ c1.healthcheck.disable()
+ output = render.render()
+ assert "cap_drop" not in output["services"]["test_container"]
+ assert "cap_add" not in output["services"]["test_container"]
+
+
+def test_privileged(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_privileged(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["privileged"] is True
+
+
+def test_tty(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_tty(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["tty"] is True
+
+
+def test_init(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_init(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["init"] is True
+
+
+def test_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_read_only(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["read_only"] is True
+
+
+def test_stdin(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_stdin(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stdin_open"] is True
+
+
+def test_hostname(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_hostname("test_hostname")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["hostname"] == "test_hostname"
+
+
+def test_grace_period(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_grace_period(10)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stop_grace_period"] == "10s"
+
+
+def test_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_user(1000, 1000)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["user"] == "1000:1000"
+
+
+def test_invalid_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_user(-100, 1000)
+
+
+def test_add_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ c1.add_group("video")
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
+
+
+def test_add_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ with pytest.raises(Exception):
+ c1.add_group(1000)
+
+
+def test_add_group_as_string(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_group("1000")
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_docker_socket()
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 999]
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {
+ "propagation": "rprivate",
+ "create_host_path": False,
+ },
+ }
+ ]
+
+
+def test_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_shm_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_shm_size_mb(10)
+ output = render.render()
+ assert output["services"]["test_container"]["shm_size"] == "10M"
+
+
+def test_valid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_caps(["ALL", "NET_ADMIN"])
+ output = render.render()
+ assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
+ assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
+
+
+def test_add_duplicate_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
+
+
+def test_invalid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["invalid_cap"])
+
+
+def test_remove_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.remove_security_opt("no-new-privileges")
+ output = render.render()
+ assert "security_opt" not in output["services"]["test_container"]
+
+
+def test_add_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_security_opt("seccomp=unconfined")
+ output = render.render()
+ assert output["services"]["test_container"]["security_opt"] == [
+ "no-new-privileges",
+ "seccomp=unconfined",
+ ]
+
+
+def test_add_duplicate_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_security_opt("no-new-privileges")
+
+
+def test_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_auto_network_mode_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_network_mode_with_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("service:test_container")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "service:test_container"
+
+
+def test_network_mode_with_container_missing(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("service:missing_container")
+
+
+def test_invalid_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("invalid_mode")
+
+
+def test_entrypoint(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
+
+
+def test_command(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_command(["echo", "hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
+
+
+def test_add_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
+ c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
+ c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
+ c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
+ c1.add_port(
+ {"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
+ {"container_port": 9092, "protocol": "udp"},
+ )
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
+ ]
+ assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
+
+
+def test_add_ports_with_invalid_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+ ]
+
+
+def test_set_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c2 = render.add_container("test_container2", "test_image")
+ c2.healthcheck.disable()
+ c1.set_ipc_mode("container:test_container2")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("container:invalid")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py
new file mode 100644
index 0000000000..a1d8373927
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_depends.py
@@ -0,0 +1,54 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_dependency(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ c1.depends.add_dependency("test_container2", "service_started")
+ output = render.render()
+ assert output["services"]["test_container"]["depends_on"]["test_container2"] == {"condition": "service_started"}
+
+
+def test_add_dependency_invalid_condition(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ render.add_container("test_container2", "test_image")
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "invalid_condition")
+
+
+def test_add_dependency_missing_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
+
+
+def test_add_dependency_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ render.add_container("test_container2", "test_image")
+ c1.depends.add_dependency("test_container2", "service_started")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py
new file mode 100644
index 0000000000..a1b7f03a60
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_deps.py
@@ -0,0 +1,477 @@
+import json
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_postgres_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ p = render.deps.postgres(
+ "pg_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_@password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ p.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert (
+ p.get_url("postgres") == "postgres://test_user:test_%40password@pg_container:5432/test_database?sslmode=disable"
+ )
+ assert "devices" not in output["services"]["pg_container"]
+ assert "reservations" not in output["services"]["pg_container"]["deploy"]["resources"]
+ assert output["services"]["pg_container"]["image"] == "postgres:16"
+ assert output["services"]["pg_container"]["user"] == "999:999"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["pg_container"]["healthcheck"] == {
+ "test": "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["pg_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/postgresql/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["pg_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "POSTGRES_USER": "test_user",
+ "POSTGRES_PASSWORD": "test_@password",
+ "POSTGRES_DB": "test_database",
+ "POSTGRES_PORT": "5432",
+ }
+ assert output["services"]["pg_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"},
+ "pg_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert output["services"]["perms_container"]["restart"] == "on-failure:1"
+
+
+def test_add_redis_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test_password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis_with_password_with_spaces(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ r = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test&password@",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ c1.environment.add_env("REDIS_URL", r.get_url("redis"))
+ if perms_container.has_actions():
+ perms_container.activate()
+ r.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["redis_container"]
+ assert "reservations" not in output["services"]["redis_container"]["deploy"]["resources"]
+ assert (
+ output["services"]["test_container"]["environment"]["REDIS_URL"]
+ == "redis://default:test%26password%40@redis_container:6379"
+ )
+ assert output["services"]["redis_container"]["image"] == "redis:latest"
+ assert output["services"]["redis_container"]["user"] == "1001:0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["redis_container"]["healthcheck"] == {
+ "test": "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["redis_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/bitnami/redis/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["redis_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "ALLOW_EMPTY_PASSWORD": "no",
+ "REDIS_PASSWORD": "test&password@",
+ "REDIS_PORT_NUMBER": "6379",
+ }
+ assert output["services"]["redis_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_mariadb_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.mariadb(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_mariadb(mock_values):
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ m = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["mariadb_container"]
+ assert "reservations" not in output["services"]["mariadb_container"]["deploy"]["resources"]
+ assert output["services"]["mariadb_container"]["image"] == "mariadb:latest"
+ assert output["services"]["mariadb_container"]["user"] == "999:999"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["mariadb_container"]["healthcheck"] == {
+ "test": "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["mariadb_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/mysql",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["mariadb_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "MARIADB_USER": "test_user",
+ "MARIADB_PASSWORD": "test_password",
+ "MARIADB_ROOT_PASSWORD": "test_password",
+ "MARIADB_DATABASE": "test_database",
+ "MARIADB_AUTO_UPGRADE": "true",
+ }
+ assert output["services"]["mariadb_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_perms_container(mock_values):
+ mock_values["ix_volumes"] = {
+ "test_dataset1": "/mnt/test/1",
+ "test_dataset2": "/mnt/test/2",
+ "test_dataset3": "/mnt/test/3",
+ }
+ mock_values["images"]["postgres_image"] = {"repository": "postgres", "tag": "17"}
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ # fmt: off
+ volume_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ volume_no_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ host_path_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "auto_permissions": True}}
+ host_path_no_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ host_path_acl_perms = {"type": "host_path", "host_path_config": {"acl": {"path": "/mnt/test"}, "acl_enable": True, "auto_permissions": True}} # noqa
+ ix_volume_no_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset1"}}
+ ix_volume_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset2", "auto_permissions": True}} # noqa
+ ix_volume_acl_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset3", "acl_enable": True, "auto_permissions": True}} # noqa
+ temp_volume = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ # fmt: on
+
+ c1.add_storage("/some/path1", volume_perms)
+ c1.add_storage("/some/path2", volume_no_perms)
+ c1.add_storage("/some/path3", host_path_perms)
+ c1.add_storage("/some/path4", host_path_no_perms)
+ c1.add_storage("/some/path5", host_path_acl_perms)
+ c1.add_storage("/some/path6", ix_volume_no_perms)
+ c1.add_storage("/some/path7", ix_volume_perms)
+ c1.add_storage("/some/path8", ix_volume_acl_perms)
+ c1.add_storage("/some/path9", temp_volume)
+
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data2", volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data3", host_path_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data4", host_path_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data5", host_path_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data6", ix_volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data7", ix_volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data8", ix_volume_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data9", temp_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+ postgres = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ redis = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test_password",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ mariadb = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ postgres.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ redis.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ mariadb.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert output["services"]["test_perms_container"]["network_mode"] == "none"
+ assert output["services"]["test_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+ assert output["configs"]["permissions_run_script"]["content"] != ""
+ # fmt: off
+ content = [
+ {"mount_path": "/mnt/permission/data", "is_temporary": False, "identifier": "data", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data3", "is_temporary": False, "identifier": "data3", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data6", "is_temporary": False, "identifier": "data6", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data7", "is_temporary": False, "identifier": "data7", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data9", "is_temporary": True, "identifier": "data9", "recursive": True, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/postgres_container_postgres_data", "is_temporary": False, "identifier": "postgres_container_postgres_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/redis_container_redis_data", "is_temporary": False, "identifier": "redis_container_redis_data", "recursive": False, "mode": "check", "uid": 1001, "gid": 0, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/mariadb_container_mariadb_data", "is_temporary": False, "identifier": "mariadb_container_mariadb_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ ]
+ # fmt: on
+ assert output["configs"]["permissions_actions_data"]["content"] == json.dumps(content)
+
+
+def test_add_duplicate_perms_action(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ with pytest.raises(Exception):
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+
+def test_add_perm_action_without_auto_perms_enabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": False}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "configs" not in output
+ assert "ix-test_perms_container" not in output["services"]
+ assert "depends_on" not in output["services"]["test_container"]
+
+
+def test_add_unsupported_postgres_version(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "99"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres_with_invalid_tag(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_no_upgrade_container_with_non_postgres_image(mock_values):
+ mock_values["images"]["postgres_image"] = {"repository": "tensorchord/pgvecto-rs", "tag": "pg15-v0.2.0"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert len(output["services"]) == 3 # c1, pg, perms
+ assert output["services"]["postgres_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_postgres_with_upgrade_container(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": 16.6}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ pg = output["services"]["postgres_container"]
+ pgup = output["services"]["postgres_container_upgrade"]
+ assert pg["volumes"] == pgup["volumes"]
+ assert pg["user"] == pgup["user"]
+ assert pgup["environment"]["TARGET_VERSION"] == "16"
+ assert pgup["environment"]["DATA_DIR"] == "/var/lib/postgresql/data"
+ pgup_env = pgup["environment"]
+ pgup_env.pop("TARGET_VERSION")
+ pgup_env.pop("DATA_DIR")
+ assert pg["environment"] == pgup_env
+ assert pg["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"},
+ "postgres_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert pgup["depends_on"] == {"test_perms_container": {"condition": "service_completed_successfully"}}
+ assert pgup["restart"] == "on-failure:1"
+ assert pgup["healthcheck"] == {"disable": True}
+ assert pgup["build"]["dockerfile_inline"] != ""
+ assert pgup["configs"][0]["source"] == "pg_container_upgrade.sh"
+ assert pgup["entrypoint"] == ["/bin/bash", "-c", "/upgrade.sh"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device.py
new file mode 100644
index 0000000000..2e71daa5a0
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device.py
@@ -0,0 +1,150 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
+
+
+def test_devices_without_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("", "/c/dev/sda")
+
+
+def test_devices_without_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "")
+
+
+def test_add_duplicate_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_container_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "c/dev/sda")
+
+
+def test_add_device_with_invalid_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("h/dev/sda", "/c/dev/sda")
+
+
+def test_add_disallowed_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/dri", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_cgroup_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
+
+
+def test_automatically_add_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_automatically_add_gpu_devices_and_kfd(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_remove_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.remove_devices()
+ output = render.render()
+ assert "devices" not in output["services"]["test_container"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_add_usb_bus(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_usb_bus()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
+
+
+def test_add_usb_bus_disallowed(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
+
+
+def test_add_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_add_tun_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_tun_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/net/tun:/dev/net/tun"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
new file mode 100644
index 0000000000..581fe82017
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_device_cgroup_rule(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ c1.add_device_cgroup_rule("b 10:20 rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["device_cgroup_rules"] == [
+ "b 10:20 rwm",
+ "c 13:* rwm",
+ ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 rwd")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py
new file mode 100644
index 0000000000..fe6b21e34f
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_dns.py
@@ -0,0 +1,64 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "opt1", "opt2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_opt"] == ["attempts:3", "opt1", "opt2"]
+
+
+def test_auto_add_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_search"] == ["search1", "search2"]
+
+
+def test_auto_add_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns"] == ["nameserver1", "nameserver2"]
+
+
+def test_add_duplicate_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "attempts:5"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py
new file mode 100644
index 0000000000..d657646582
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_environment.py
@@ -0,0 +1,196 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ mock_values["run_as"] = {"user": "1000", "group": "1000"}
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert len(envs) == 11
+ assert envs["TZ"] == "Etc/UTC"
+ assert envs["PUID"] == "1000"
+ assert envs["UID"] == "1000"
+ assert envs["USER_ID"] == "1000"
+ assert envs["PGID"] == "1000"
+ assert envs["GID"] == "1000"
+ assert envs["GROUP_ID"] == "1000"
+ assert envs["UMASK"] == "002"
+ assert envs["UMASK_SET"] == "002"
+ assert envs["NVIDIA_DRIVER_CAPABILITIES"] == "all"
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "uuid_0,uuid_1"
+
+
+def test_skip_generic_variables(mock_values):
+ mock_values["skip_generic_variables"] = True
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+
+ assert len(envs) == 1
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
+
+
+def test_add_from_all_sources(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_value")
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_value"
+ assert envs["USER_ENV"] == "test_value2"
+ assert envs["TZ"] == "Etc/UTC"
+
+
+def test_user_add_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV2", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["MY_ENV"] == "test_value"
+ assert envs["MY_ENV2"] == "test_value2"
+
+
+def test_user_add_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV", "value": "test_value2"},
+ ]
+ )
+
+
+def test_user_env_without_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "", "value": "test_value"},
+ ]
+ )
+
+
+def test_user_env_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "TZ", "value": "test_value"},
+ ]
+ )
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_user_env_try_to_overwrite_app_dev_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "PORT", "value": "test_value"},
+ ]
+ )
+ c1.environment.add_env("PORT", "test_value2")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_vars_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("TZ", "test_value")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_no_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_env("", "test_value")
+
+
+def test_app_dev_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("PORT", "test_value")
+ with pytest.raises(Exception):
+ c1.environment.add_env("PORT", "test_value2")
+
+
+def test_format_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_$value")
+ c1.environment.add_env("APP_ENV_BOOL", True)
+ c1.environment.add_env("APP_ENV_INT", 10)
+ c1.environment.add_env("APP_ENV_FLOAT", 10.5)
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_$value2"},
+ ]
+ )
+
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_$$value"
+ assert envs["USER_ENV"] == "test_$$value2"
+ assert envs["APP_ENV_BOOL"] == "true"
+ assert envs["APP_ENV_INT"] == "10"
+ assert envs["APP_ENV_FLOAT"] == "10.5"
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py
new file mode 100644
index 0000000000..b8724d7548
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_expose.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ c1.expose.add_port(8081, "udp")
+ c1.expose.add_port(8082, "udp")
+ output = render.render()
+ assert output["services"]["test_container"]["expose"] == ["8081/tcp", "8081/udp", "8082/udp"]
+
+
+def test_add_duplicate_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ with pytest.raises(Exception):
+ c1.expose.add_port(8081)
+
+
+def test_add_expose_ports_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ output = render.render()
+ assert "expose" not in output["services"]["test_container"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py
new file mode 100644
index 0000000000..35230be16e
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_extra_hosts.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ c1.add_extra_host("test_host2", "127.0.0.2")
+ c1.add_extra_host("host.docker.internal", "host-gateway")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {
+ "host.docker.internal": "host-gateway",
+ "test_host": "127.0.0.1",
+ "test_host2": "127.0.0.2",
+ }
+
+
+def test_add_duplicate_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "::1")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "invalid_ip")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py
new file mode 100644
index 0000000000..843cf65d2e
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_formatter.py
@@ -0,0 +1,13 @@
+from formatter import escape_dollar
+
+
+def test_escape_dollar():
+ cases = [
+ {"input": "test", "expected": "test"},
+ {"input": "$test", "expected": "$$test"},
+ {"input": "$$test", "expected": "$$$$test"},
+ {"input": "$$$test", "expected": "$$$$$$test"},
+ {"input": "$test$", "expected": "$$test$$"},
+ ]
+ for case in cases:
+ assert escape_dollar(case["input"]) == case["expected"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py
new file mode 100644
index 0000000000..0ea3b57d18
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_functions.py
@@ -0,0 +1,88 @@
+import re
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_funcs(mock_values):
+ mock_values["ix_volumes"] = {"test": "/mnt/test123"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ tests = [
+ {"func": "auto_cast", "values": ["1"], "expected": 1},
+ {"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
+ {"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
+ {
+ "func": "bcrypt_hash",
+ "values": ["my_pass"],
+ "expect_regex": r"^\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "camel_case", "values": ["my_user"], "expected": "My_User"},
+ {"func": "copy_dict", "values": [{"a": 1}], "expected": {"a": 1}},
+ {"func": "fail", "values": ["my_message"], "expect_raise": True},
+ {
+ "func": "htpasswd",
+ "values": ["my_user", "my_pass"],
+ "expect_regex": r"^my_user:\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "is_boolean", "values": ["true"], "expected": True},
+ {"func": "is_boolean", "values": ["false"], "expected": True},
+ {"func": "is_number", "values": ["1"], "expected": True},
+ {"func": "is_number", "values": ["1.1"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[a-zA-Z0-9]+$"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[0-9]+$"], "expected": False},
+ {"func": "merge_dicts", "values": [{"a": 1}, {"b": 2}], "expected": {"a": 1, "b": 2}},
+ {"func": "must_match_regex", "values": ["my_user", "^[0-9]$"], "expect_raise": True},
+ {"func": "must_match_regex", "values": ["1", "^[0-9]$"], "expected": "1"},
+ {"func": "secure_string", "values": [10], "expect_regex": r"^[a-zA-Z0-9-_]+$"},
+ {"func": "disallow_chars", "values": ["my_user", ["$", "@"], "my_key"], "expected": "my_user"},
+ {"func": "disallow_chars", "values": ["my_user$", ["$", "@"], "my_key"], "expect_raise": True},
+ {
+ "func": "get_host_path",
+ "values": [{"type": "host_path", "host_path_config": {"path": "/mnt/test"}}],
+ "expected": "/mnt/test",
+ },
+ {
+ "func": "get_host_path",
+ "values": [{"type": "ix_volume", "ix_volume_config": {"dataset_name": "test"}}],
+ "expected": "/mnt/test123",
+ },
+ {"func": "or_default", "values": [None, 1], "expected": 1},
+ {"func": "or_default", "values": [1, None], "expected": 1},
+ {"func": "or_default", "values": [False, 1], "expected": 1},
+ {"func": "or_default", "values": [True, 1], "expected": True},
+ {"func": "temp_config", "values": [""], "expect_raise": True},
+ {
+ "func": "temp_config",
+ "values": ["test"],
+ "expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
+ },
+ ]
+
+ for test in tests:
+ print(test["func"], test)
+ func = render.funcs[test["func"]]
+ if test.get("expect_raise", False):
+ with pytest.raises(Exception):
+ func(*test["values"])
+ elif test.get("expect_regex"):
+ r = func(*test["values"])
+ assert re.match(test["expect_regex"], r) is not None
+ else:
+ r = func(*test["values"])
+ assert r == test["expected"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py
new file mode 100644
index 0000000000..8fa044290f
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_healthcheck.py
@@ -0,0 +1,195 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_disable_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {"disable": True}
+
+
+def test_use_built_in_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.use_built_in()
+ output = render.render()
+ assert "healthcheck" not in output["services"]["test_container"]
+
+
+def test_set_custom_test(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test("echo $1")
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": "echo $$1",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_custom_test_array(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ c1.healthcheck.set_interval(9)
+ c1.healthcheck.set_timeout(8)
+ c1.healthcheck.set_retries(7)
+ c1.healthcheck.set_start_period(6)
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "9s",
+ "timeout": "8s",
+ "retries": 7,
+ "start_period": "6s",
+ }
+
+
+def test_adding_test_when_disabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.healthcheck.set_custom_test("echo $1")
+
+
+def test_not_adding_test(mock_values):
+ render = Render(mock_values)
+ render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_invalid_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.healthcheck.set_test("http", {"port": 8080, "path": "invalid"})
+
+
+def test_http_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("http", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == """/bin/bash -c 'exec {hc_fd}<>/dev/tcp/127.0.0.1/8080 && echo -e "GET / HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n" >&$${hc_fd} && cat <&$${hc_fd} | grep "HTTP" | grep -q "200"'""" # noqa
+ )
+
+
+def test_curl_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "curl --silent --output /dev/null --show-error --fail http://127.0.0.1:8080/health"
+ )
+
+
+def test_curl_healthcheck_with_headers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "headers": [("X-Test", "$test")]})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == 'curl --silent --output /dev/null --show-error --fail --header "X-Test: $$test" http://127.0.0.1:8080/health'
+ )
+
+
+def test_wget_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "wget --spider --quiet http://127.0.0.1:8080/health"
+ )
+
+
+def test_netcat_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("netcat", {"port": 8080})
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"]["test"] == "nc -z -w 1 127.0.0.1 8080"
+
+
+def test_tcp_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("tcp", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/8080'"
+ )
+
+
+def test_redis_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("redis")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG"
+ )
+
+
+def test_postgres_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("postgres")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB"
+ )
+
+
+def test_mariadb_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("mariadb")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping"
+ )
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py
new file mode 100644
index 0000000000..ffa21eceac
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_labels.py
@@ -0,0 +1,88 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_disallowed_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.labels.add_label("com.docker.compose.service", "test_service")
+
+
+def test_add_duplicate_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label", "test_value")
+ with pytest.raises(Exception):
+ c1.labels.add_label("my.custom.label", "test_value1")
+
+
+def test_add_label_on_non_existing_container(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label1", "test_value1")
+ c1.labels.add_label("my.custom.label2", "test_value2")
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+
+
+def test_auto_add_labels(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ {
+ "key": "my.custom.label2",
+ "value": "test_value2",
+ "containers": ["test_container"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+ assert output["services"]["test_container2"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ }
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py
new file mode 100644
index 0000000000..3bdfe33c74
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_notes.py
@@ -0,0 +1,184 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "ix_context": {
+ "app_metadata": {
+ "name": "test_app",
+ "title": "Test App",
+ "train": "enterprise",
+ }
+ },
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_notes(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_on_non_enterprise_train(mock_values):
+ mock_values["ix_context"]["app_metadata"]["train"] = "community"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://github.com/truenas/apps
+
+"""
+ )
+
+
+def test_notes_with_warnings(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_deprecations(mock_values):
+ render = Render(mock_values)
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_body(mock_values):
+ render = Render(mock_values)
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_all(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py
new file mode 100644
index 0000000000..aebd9425c9
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_portal.py
@@ -0,0 +1,75 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_no_portals(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == []
+
+
+def test_add_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ render.portals.add_portal({"name": "Other Portal", "scheme": "https", "path": "/", "port": 8443})
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == [
+ {"name": "Other Portal", "scheme": "https", "host": "0.0.0.0", "port": 8443, "path": "/"},
+ {"name": "Web UI", "scheme": "http", "host": "0.0.0.0", "port": 8080, "path": "/"},
+ ]
+
+
+def test_add_duplicate_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_duplicate_portal_with_explicit_name(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_scheme(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "invalid_scheme", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_path(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "invalid_path", "port": 8080})
+
+
+def test_add_portal_with_invalid_path_double_slash(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/some//path", "port": 8080})
+
+
+def test_add_portal_with_invalid_port(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": -1})
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py
new file mode 100644
index 0000000000..7b37a03600
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_ports.py
@@ -0,0 +1,209 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+tests = [
+ {
+ "name": "add_ports_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8082, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "add_duplicate_ports_should_fail",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_different_protocol_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_same_port_for_both_wildcard_families_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {
+ "published": 8081,
+ "target": 8080,
+ "protocol": "tcp",
+ "mode": "ingress",
+ "host_ip": "fd00:1234:5678:abcd::10",
+ },
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_with_different_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_protocol_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"protocol": "invalid_protocol"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_mode_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"mode": "invalid_mode"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "invalid_ip"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_host_port_should_fail",
+ "inputs": [
+ {"values": (-1, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_container_port_should_fail",
+ "inputs": [
+ {"values": (8081, -1), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_ports_with_different_host_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::11"}), "expect_error": False},
+ ],
+ # fmt: off
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ # fmt: on
+ },
+]
+
+
+@pytest.mark.parametrize("test", tests)
+def test_ports(test):
+ mock_values = {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ errored = False
+ for input in test["inputs"]:
+ if input["expect_error"]:
+ with pytest.raises(Exception):
+ c1.ports.add_port(*input["values"])
+ errored = True
+ else:
+ c1.ports.add_port(*input["values"])
+
+ errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False
+ if errored:
+ return
+
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == test["expected"]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_render.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_render.py
new file mode 100644
index 0000000000..60dc00679e
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_render.py
@@ -0,0 +1,37 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_values_cannot_be_modified(mock_values):
+ render = Render(mock_values)
+ render.values["test"] = "test"
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_duplicate_containers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_no_containers(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py
new file mode 100644
index 0000000000..cd83d164e5
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_resources.py
@@ -0,0 +1,140 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_automatically_add_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": 1.0}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1.0"
+
+
+def test_invalid_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": 1024}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "1024M"
+
+
+def test_invalid_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_gpus(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_0", "uuid_1"],
+ }
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_gpu_without_uuid(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_remove_cpus_and_memory_with_gpus(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_1", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "limits" not in output["services"]["test_container"]["deploy"]["resources"]
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_1"],
+ }
+
+
+def test_remove_cpus_and_memory(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "deploy" not in output["services"]["test_container"]
+
+
+def test_remove_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_devices()
+ output = render.render()
+ assert "reservations" not in output["services"]["test_container"]["deploy"]["resources"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_set_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.set_profile("low")
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1"
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "512M"
+
+
+def test_set_profile_invalid_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.deploy.resources.set_profile("invalid_profile")
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py
new file mode 100644
index 0000000000..06b2975590
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_restart.py
@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_invalid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("invalid_policy")
+
+
+def test_valid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure")
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure"
+
+
+def test_valid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure", 10)
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure:10"
+
+
+def test_invalid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("on-failure", maximum_retry_count=-1)
+
+
+def test_invalid_restart_policy_with_maximum_retry_count_and_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("always", maximum_retry_count=10)
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py
new file mode 100644
index 0000000000..c9414044ea
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_sysctls.py
@@ -0,0 +1,62 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ c1.sysctls.add("fs.mqueue.msg_max", 100)
+ output = render.render()
+ assert output["services"]["test_container"]["sysctls"] == {"net.ipv4.ip_forward": "1", "fs.mqueue.msg_max": "100"}
+
+
+def test_add_net_sysctl_with_host_network(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_duplicate_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ c1.sysctls.add("net.ipv4.ip_forward", 0)
+
+
+def test_add_empty_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.sysctls.add("", 1)
+
+
+def test_add_sysctl_with_invalid_key(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("invalid.sysctl", 1)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_validations.py
new file mode 100644
index 0000000000..f0986ce9a5
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/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/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py
new file mode 100644
index 0000000000..9a98956bdc
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/tests/test_volumes.py
@@ -0,0 +1,727 @@
+import pytest
+
+
+from render import Render
+from formatter import get_hashed_name_for_volume
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_volume_invalid_type(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "invalid_type"})
+
+
+def test_add_volume_empty_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("", {"type": "tmpfs"})
+
+
+def test_add_volume_duplicate_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+
+
+def test_add_volume_host_path_invalid_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_host_path_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_with_acl_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_mount(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_acl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
+ }
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test/acl",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "slave"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_create_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_invalid_dataset_name(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_no_ix_volume_config(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_volume_mount(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_volume_mount_with_options(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {
+ "type": "ix_volume",
+ "ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
+ }
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rslave"},
+ }
+ ]
+
+
+def test_cifs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_username(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_password(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_without_cifs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["verbose=true", "verbose=true"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["user=username"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": {"verbose": True},
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": [{"verbose": True}],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_add_cifs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_cifs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["vers=3.0", "verbose=true"],
+ }
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "cifs",
+ "device": "//server/path",
+ "o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_without_nfs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {
+ "type": "nfs",
+ "nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_add_nfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path"}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "nfs",
+ "device": ":/path",
+ "o": "addr=server,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_tmpfs_invalid_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_zero_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_invalid_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs"}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "tmpfs",
+ "target": "/some/path",
+ "read_only": False,
+ }
+ ]
+
+
+def test_temporary_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "source": "test_temp_volume",
+ "type": "volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+
+
+def test_docker_volume_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume_missing_volume_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["volumes"] == {"test_volume": {}}
+
+
+def test_anonymous_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
+ ]
+ assert "volumes" not in output
+
+
+def test_add_udev(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev()
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_udev_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev(read_only=False)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/some/path")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_host_path_with_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_host_path_without_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/mnt", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/mnt",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/validations.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/validations.py
new file mode 100644
index 0000000000..d4aa633006
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/validations.py
@@ -0,0 +1,316 @@
+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_port_bind_mode_or_raise(status: str):
+ valid_statuses = ("published", "exposed", "")
+ if status not in valid_statuses:
+ raise RenderError(f"Invalid port status [{status}]")
+ return status
+
+
+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_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+ valid_modes = ("", "host", "private", "shareable", "none")
+ if ipc_mode in valid_modes:
+ return ipc_mode
+ if ipc_mode.startswith("container:"):
+ if ipc_mode[10:] not in containers:
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+ return ipc_mode
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+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 valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+ parts = dev_grp_rule.split(" ")
+ if len(parts) != 3:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [ : ]"
+ )
+
+ valid_types = ("a", "b", "c")
+ if parts[0] not in valid_types:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+ f" but got [{parts[0]}]"
+ )
+
+ major, minor = parts[1].split(":")
+ for part in (major, minor):
+ if part != "*" and not part.isdigit():
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+ f" or [*] but got [{major}] and [{minor}]"
+ )
+
+ valid_cgroup_perm_or_raise(parts[2])
+
+ return dev_grp_rule
+
+
+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/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount.py
new file mode 100644
index 0000000000..aadd077750
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount.py
@@ -0,0 +1,92 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .formatter import merge_dicts_no_overwrite
+ from .volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from .volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+except ImportError:
+ from error import RenderError
+ from formatter import merge_dicts_no_overwrite
+ from volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+
+
+class VolumeMount:
+ def __init__(self, render_instance: "Render", mount_path: str, config: "IxStorage"):
+ self._render_instance = render_instance
+ self.mount_path: str = mount_path
+
+ storage_type: str = config.get("type", "")
+ if not storage_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match storage_type:
+ case "host_path":
+ spec_type = "bind"
+ mount_config = config.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = HostPathSource(self._render_instance, mount_config).get()
+ case "ix_volume":
+ spec_type = "bind"
+ mount_config = config.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = IxVolumeSource(self._render_instance, mount_config).get()
+ case "tmpfs":
+ spec_type = "tmpfs"
+ mount_config = config.get("tmpfs_config", {})
+ mount_type_specific_definition = TmpfsMountType(self._render_instance, mount_config).render()
+ source = None
+ case "nfs":
+ spec_type = "volume"
+ mount_config = config.get("nfs_config")
+ if mount_config is None:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = NfsSource(self._render_instance, mount_config).get()
+ case "cifs":
+ spec_type = "volume"
+ mount_config = config.get("cifs_config")
+ if mount_config is None:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = CifsSource(self._render_instance, mount_config).get()
+ case "volume":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [volume] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "temporary":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [temporary] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "anonymous":
+ spec_type = "volume"
+ mount_config = config.get("volume_config") or {}
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = None
+ case _:
+ raise RenderError(f"Storage type [{storage_type}] is not supported for volume mounts.")
+
+ common_spec = {"type": spec_type, "target": self.mount_path, "read_only": config.get("read_only", False)}
+ if source is not None:
+ common_spec["source"] = source
+ self._render_instance.volumes.add_volume(source, storage_type, mount_config) # type: ignore
+
+ self.volume_mount_spec = merge_dicts_no_overwrite(common_spec, mount_type_specific_definition)
+
+ def render(self) -> dict:
+ return self.volume_mount_spec
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py
new file mode 100644
index 0000000000..00a0ec3a18
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_mount_types.py
@@ -0,0 +1,72 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageTmpfsConfig, IxStorageVolumeConfig, IxStorageBindLikeConfigs
+
+
+try:
+ from .error import RenderError
+ from .validations import valid_host_path_propagation, valid_octal_mode_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_host_path_propagation, valid_octal_mode_or_raise
+
+
+class TmpfsMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageTmpfsConfig"):
+ self._render_instance = render_instance
+ self.spec = {"tmpfs": {}}
+ size = config.get("size", None)
+ mode = config.get("mode", None)
+
+ if size is not None:
+ if not isinstance(size, int):
+ raise RenderError(f"Expected [size] to be an integer for [tmpfs] type, got [{size}]")
+ if not size > 0:
+ raise RenderError(f"Expected [size] to be greater than 0 for [tmpfs] type, got [{size}]")
+ # Convert Mebibytes to Bytes
+ self.spec["tmpfs"]["size"] = size * 1024 * 1024
+
+ if mode is not None:
+ mode = valid_octal_mode_or_raise(mode)
+ self.spec["tmpfs"]["mode"] = int(mode, 8)
+
+ if not self.spec["tmpfs"]:
+ self.spec.pop("tmpfs")
+
+ def render(self) -> dict:
+ """Render the tmpfs mount specification."""
+ return self.spec
+
+
+class BindMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageBindLikeConfigs"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ propagation = valid_host_path_propagation(config.get("propagation", "rprivate"))
+ create_host_path = config.get("create_host_path", False)
+
+ self.spec: dict = {
+ "bind": {
+ "create_host_path": create_host_path,
+ "propagation": propagation,
+ }
+ }
+
+ def render(self) -> dict:
+ """Render the bind mount specification."""
+ return self.spec
+
+
+class VolumeMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ self.spec: dict = {"volume": {"nocopy": config.get("nocopy", False)}}
+
+ def render(self) -> dict:
+ """Render the volume mount specification."""
+ return self.spec
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_sources.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_sources.py
new file mode 100644
index 0000000000..6aba23df23
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_sources.py
@@ -0,0 +1,110 @@
+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("/")
+ # TODO: Hack for Nextcloud deprecated config. Remove once we remove support for it
+ allow_unsafe_ix_volume = config.get("allow_unsafe_ix_volume", False)
+ self.source = allowed_fs_host_path_or_raise(path, allow_unsafe_ix_volume)
+
+ 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/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_types.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_types.py
new file mode 100644
index 0000000000..4ccea08f83
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volume_types.py
@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageNfsConfig, IxStorageCifsConfig, IxStorageVolumeConfig
+
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_fs_path_or_raise
+
+
+class NfsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageNfsConfig"):
+ self._render_instance = render_instance
+
+ if not config:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type")
+
+ required_keys = ["server", "path"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [nfs] type")
+
+ opts = [f"addr={config['server']}"]
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [nfs_config.options] to be a list for [nfs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["addr"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [nfs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [nfs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [nfs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+
+ opts.sort()
+
+ path = valid_fs_path_or_raise(config["path"].rstrip("/"))
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "nfs",
+ "device": f":{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class CifsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageCifsConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ if not config:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type")
+
+ required_keys = ["server", "path", "username", "password"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [cifs] type")
+
+ opts = [
+ "noperm",
+ f"user={config['username']}",
+ f"password={config['password']}",
+ ]
+
+ domain = config.get("domain")
+ if domain:
+ opts.append(f"domain={domain}")
+
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [cifs_config.options] to be a list for [cifs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["user", "password", "domain", "noperm"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [cifs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [cifs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ for disallowed in disallowed_opts:
+ if key == disallowed:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+ opts.sort()
+
+ server = config["server"].lstrip("/")
+ path = config["path"].strip("/")
+ path = valid_fs_path_or_raise("/" + path).lstrip("/")
+
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "cifs",
+ "device": f"//{server}/{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class DockerVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ def get(self):
+ return self.volume_spec
diff --git a/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volumes.py b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volumes.py
new file mode 100644
index 0000000000..e6925a402f
--- /dev/null
+++ b/trains/stable/netdata/1.2.12/templates/library/base_v2_1_14/volumes.py
@@ -0,0 +1,66 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+try:
+ from .error import RenderError
+ from .storage import IxStorageVolumeLikeConfigs
+ from .volume_types import NfsVolume, CifsVolume, DockerVolume
+except ImportError:
+ from error import RenderError
+ from storage import IxStorageVolumeLikeConfigs
+ from volume_types import NfsVolume, CifsVolume, DockerVolume
+
+
+class Volumes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volumes: dict[str, Volume] = {}
+
+ def add_volume(
+ self,
+ source: str,
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ # This method can be called many times from the volume mounts
+ # Only add the volume if it is not already added, but dont raise an error
+ if source == "":
+ raise RenderError(f"Volume source [{source}] cannot be empty")
+
+ if source in self._volumes:
+ return
+
+ self._volumes[source] = Volume(self._render_instance, storage_type, config)
+
+ def has_volumes(self) -> bool:
+ return bool(self._volumes)
+
+ def render(self):
+ return {name: v.render() for name, v in sorted(self._volumes.items()) if v.render() is not None}
+
+
+class Volume:
+ def __init__(
+ self,
+ render_instance: "Render",
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ self._render_instance = render_instance
+ self.volume_spec: dict | None = {}
+
+ match storage_type:
+ case "nfs":
+ self.volume_spec = NfsVolume(self._render_instance, config).get() # type: ignore
+ case "cifs":
+ self.volume_spec = CifsVolume(self._render_instance, config).get() # type: ignore
+ case "volume" | "temporary":
+ self.volume_spec = DockerVolume(self._render_instance, config).get() # type: ignore
+ case _:
+ self.volume_spec = None
+
+ def render(self):
+ return self.volume_spec
diff --git a/trains/stable/netdata/1.2.11/templates/test_values/basic-values.yaml b/trains/stable/netdata/1.2.12/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/stable/netdata/1.2.11/templates/test_values/basic-values.yaml
rename to trains/stable/netdata/1.2.12/templates/test_values/basic-values.yaml
diff --git a/trains/stable/netdata/app_versions.json b/trains/stable/netdata/app_versions.json
index e3566b54ce..a2635e2fee 100644
--- a/trains/stable/netdata/app_versions.json
+++ b/trains/stable/netdata/app_versions.json
@@ -1,15 +1,15 @@
{
- "1.2.11": {
+ "1.2.12": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/stable/netdata/1.2.11",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/stable/netdata/1.2.12",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "v2.2.1_1.2.11",
- "version": "1.2.11",
+ "human_version": "v2.2.2_1.2.12",
+ "version": "1.2.12",
"app_metadata": {
- "app_version": "v2.2.1",
+ "app_version": "v2.2.2",
"capabilities": [
{
"description": "Netdata is able to chown files.",
@@ -105,7 +105,7 @@
],
"title": "Netdata",
"train": "stable",
- "version": "1.2.11"
+ "version": "1.2.12"
},
"schema": {
"groups": [
diff --git a/trains/stable/nextcloud/1.5.18/README.md b/trains/stable/nextcloud/1.6.0/README.md
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/README.md
rename to trains/stable/nextcloud/1.6.0/README.md
diff --git a/trains/stable/nextcloud/1.5.18/app.yaml b/trains/stable/nextcloud/1.6.0/app.yaml
similarity index 99%
rename from trains/stable/nextcloud/1.5.18/app.yaml
rename to trains/stable/nextcloud/1.6.0/app.yaml
index c90a90a205..22a8472c0a 100644
--- a/trains/stable/nextcloud/1.5.18/app.yaml
+++ b/trains/stable/nextcloud/1.6.0/app.yaml
@@ -73,4 +73,4 @@ sources:
- https://github.com/truenas/charts/tree/master/charts/nextcloud
title: Nextcloud
train: stable
-version: 1.5.18
+version: 1.6.0
diff --git a/trains/stable/nextcloud/1.5.18/ix_values.yaml b/trains/stable/nextcloud/1.6.0/ix_values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/ix_values.yaml
rename to trains/stable/nextcloud/1.6.0/ix_values.yaml
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migrate_from_kubernetes b/trains/stable/nextcloud/1.6.0/migrations/migrate_from_kubernetes
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migrate_from_kubernetes
rename to trains/stable/nextcloud/1.6.0/migrations/migrate_from_kubernetes
diff --git a/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/__init__.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/cpu.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/cpu.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/cpu.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/cpu.py
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/dns_config.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/dns_config.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/dns_config.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/dns_config.py
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/kubernetes_secrets.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/kubernetes_secrets.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/kubernetes_secrets.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/kubernetes_secrets.py
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/memory.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/memory.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/memory.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/memory.py
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/resources.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/resources.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/resources.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/resources.py
diff --git a/trains/stable/nextcloud/1.5.18/migrations/migration_helpers/storage.py b/trains/stable/nextcloud/1.6.0/migrations/migration_helpers/storage.py
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/migrations/migration_helpers/storage.py
rename to trains/stable/nextcloud/1.6.0/migrations/migration_helpers/storage.py
diff --git a/trains/stable/nextcloud/1.5.18/questions.yaml b/trains/stable/nextcloud/1.6.0/questions.yaml
similarity index 98%
rename from trains/stable/nextcloud/1.5.18/questions.yaml
rename to trains/stable/nextcloud/1.6.0/questions.yaml
index 9ae5261d9a..0989f000ef 100644
--- a/trains/stable/nextcloud/1.5.18/questions.yaml
+++ b/trains/stable/nextcloud/1.6.0/questions.yaml
@@ -310,6 +310,18 @@ questions:
max: 65535
show_if: [["use_different_port", "=", true]]
required: true
+ - variable: custom_confs
+ label: Custom Nginx Configurations
+ description: List of custom Nginx configurations.
+ schema:
+ type: list
+ default: []
+ items:
+ - variable: conf
+ label: Configuration
+ schema:
+ type: hostpath
+ required: true
- variable: storage
label: ""
diff --git a/trains/stable/nextcloud/1.5.18/templates/docker-compose.yaml b/trains/stable/nextcloud/1.6.0/templates/docker-compose.yaml
similarity index 96%
rename from trains/stable/nextcloud/1.5.18/templates/docker-compose.yaml
rename to trains/stable/nextcloud/1.6.0/templates/docker-compose.yaml
index 63b505d07b..8d4bab0490 100644
--- a/trains/stable/nextcloud/1.5.18/templates/docker-compose.yaml
+++ b/trains/stable/nextcloud/1.6.0/templates/docker-compose.yaml
@@ -1,5 +1,5 @@
{% from "macros/nc.jinja.sh" import occ, hosts_update, trusted_domains_update, imaginary_url %}
-{% from "macros/nc.jinja.conf" import opcache, php, limit_request_body, nginx_conf %}
+{% from "macros/nc.jinja.conf" import opcache, php, limit_request_body, use_x_real_ip_in_logs, nginx_conf %}
{% set tpl = ix_lib.base.render.Render(values) %}
@@ -116,6 +116,7 @@
{% do nc_env.x.append(("APACHE_DISABLE_REWRITE_IP", 1)) %}
{% do nc_env.x.append(("OVERWRITEPROTOCOL", "https")) %}
{% do nc_env.x.append(("TRUSTED_PROXIES", ["127.0.0.1", "192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8"] | join(" "))) %}
+ {% do nc_confs.append(("logformat.conf", use_x_real_ip_in_logs(), "/etc/apache2/conf-enabled/logformat.conf", "")) %}
{% if values.nextcloud.host and values.network.nginx.use_different_port %}
{% set host.x = "%s:%d"|format(values.nextcloud.host, values.network.nginx.external_port) %}
{% do nc_env.x.append(("OVERWRITEHOST", host.x)) %}
@@ -199,6 +200,9 @@
{% do nginx_container.configs.add("private", values.ix_certificates[values.network.certificate_id].privatekey, values.consts.ssl_key_path) %}
{% do nginx_container.configs.add("public", values.ix_certificates[values.network.certificate_id].certificate, values.consts.ssl_cert_path) %}
{% do nginx_container.configs.add("nginx.conf", nginx_conf(values), "/etc/nginx/nginx.conf", "0600") %}
+ {% for conf_path in values.network.nginx.custom_confs %}
+ {% do nginx_container.add_storage("/etc/nginx/includes/%d.conf"|format(loop.index0), {"type": "host_path", "host_path_config": {"path": conf_path}}) %}
+ {% endfor %}
{% do nginx_container.add_storage("/tmp", {"type": "anonymous", "volume_config": {}}) %}
{% do nginx_container.healthcheck.set_test("curl", {
"port": values.network.web_port, "path": "/status.php",
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/__init__.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/configs.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/configs.py
new file mode 100644
index 0000000000..b76f4b169c
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/configs.py
@@ -0,0 +1,86 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class Configs:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._configs: dict[str, dict] = {}
+
+ def add(self, name: str, data: str):
+ if not isinstance(data, str):
+ raise RenderError(f"Expected [data] to be a string, got [{type(data)}]")
+
+ if name not in self._configs:
+ self._configs[name] = {"name": name, "data": data}
+ return
+
+ if data == self._configs[name]["data"]:
+ return
+
+ raise RenderError(f"Config [{name}] already added with different data")
+
+ def has_configs(self):
+ return bool(self._configs)
+
+ def render(self):
+ return {
+ c["name"]: {"content": escape_dollar(c["data"])}
+ for c in sorted(self._configs.values(), key=lambda c: c["name"])
+ }
+
+
+class ContainerConfigs:
+ def __init__(self, render_instance: "Render", configs: Configs):
+ self._render_instance = render_instance
+ self.top_level_configs: Configs = configs
+ self.container_configs: set[ContainerConfig] = set()
+
+ def add(self, name: str, data: str, target: str, mode: str = ""):
+ self.top_level_configs.add(name, data)
+
+ if target == "":
+ raise RenderError(f"Expected [target] to be set for config [{name}]")
+ if mode != "":
+ mode = valid_octal_mode_or_raise(mode)
+
+ if target in [c.target for c in self.container_configs]:
+ raise RenderError(f"Target [{target}] already used for another config")
+ target = valid_fs_path_or_raise(target)
+ self.container_configs.add(ContainerConfig(self._render_instance, name, target, mode))
+
+ def has_configs(self):
+ return bool(self.container_configs)
+
+ def render(self):
+ return [c.render() for c in sorted(self.container_configs, key=lambda c: c.source)]
+
+
+class ContainerConfig:
+ def __init__(self, render_instance: "Render", source: str, target: str, mode: str):
+ self._render_instance = render_instance
+ self.source = source
+ self.target = target
+ self.mode = mode
+
+ def render(self):
+ result: dict[str, str | int] = {
+ "source": self.source,
+ "target": self.target,
+ }
+
+ if self.mode:
+ result["mode"] = int(self.mode, 8)
+
+ return result
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/container.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/container.py
new file mode 100644
index 0000000000..8e889be045
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/container.py
@@ -0,0 +1,418 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .configs import ContainerConfigs
+ from .depends import Depends
+ from .deploy import Deploy
+ from .device_cgroup_rules import DeviceCGroupRules
+ from .devices import Devices
+ from .dns import Dns
+ from .environment import Environment
+ from .error import RenderError
+ from .expose import Expose
+ from .extra_hosts import ExtraHosts
+ from .formatter import escape_dollar, get_image_with_hashed_data
+ from .healthcheck import Healthcheck
+ from .labels import Labels
+ from .ports import Ports
+ from .restart import RestartPolicy
+ from .validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from .storage import Storage
+ from .sysctls import Sysctls
+except ImportError:
+ from configs import ContainerConfigs
+ from depends import Depends
+ from deploy import Deploy
+ from device_cgroup_rules import DeviceCGroupRules
+ from devices import Devices
+ from dns import Dns
+ from environment import Environment
+ from error import RenderError
+ from expose import Expose
+ from extra_hosts import ExtraHosts
+ from formatter import escape_dollar, get_image_with_hashed_data
+ from healthcheck import Healthcheck
+ from labels import Labels
+ from ports import Ports
+ from restart import RestartPolicy
+ from validations import (
+ valid_cap_or_raise,
+ valid_ipc_mode_or_raise,
+ valid_network_mode_or_raise,
+ valid_port_bind_mode_or_raise,
+ valid_pull_policy_or_raise,
+ )
+ from storage import Storage
+ from sysctls import Sysctls
+
+
+class Container:
+ def __init__(self, render_instance: "Render", name: str, image: str):
+ self._render_instance = render_instance
+
+ self._name: str = name
+ self._image: str = self._resolve_image(image)
+ self._build_image: str = ""
+ self._pull_policy: str = ""
+ self._user: str = ""
+ self._tty: bool = False
+ self._stdin_open: bool = False
+ self._init: bool | None = None
+ self._read_only: bool | None = None
+ self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
+ self._hostname: str = ""
+ self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
+ self._cap_add: set[str] = set()
+ self._security_opt: set[str] = set(["no-new-privileges"])
+ self._privileged: bool = False
+ self._group_add: set[int | str] = set()
+ self._network_mode: str = ""
+ self._entrypoint: list[str] = []
+ self._command: list[str] = []
+ self._grace_period: int | None = None
+ self._shm_size: int | None = None
+ self._storage: Storage = Storage(self._render_instance)
+ self._ipc_mode: str | None = None
+ self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
+ self.sysctls: Sysctls = Sysctls(self._render_instance, self)
+ self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
+ self.deploy: Deploy = Deploy(self._render_instance)
+ self.networks: set[str] = set()
+ self.devices: Devices = Devices(self._render_instance)
+ self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
+ self.dns: Dns = Dns(self._render_instance)
+ self.depends: Depends = Depends(self._render_instance)
+ self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
+ self.labels: Labels = Labels(self._render_instance)
+ self.restart: RestartPolicy = RestartPolicy(self._render_instance)
+ self.ports: Ports = Ports(self._render_instance)
+ self.expose: Expose = Expose(self._render_instance)
+
+ self._auto_set_network_mode()
+ self._auto_add_labels()
+ self._auto_add_groups()
+
+ def _auto_add_groups(self):
+ self.add_group(568)
+
+ def _auto_set_network_mode(self):
+ if self._render_instance.values.get("network", {}).get("host_network", False):
+ self.set_network_mode("host")
+
+ def _auto_add_labels(self):
+ labels = self._render_instance.values.get("labels", [])
+ if not labels:
+ return
+
+ for label in labels:
+ containers = label.get("containers", [])
+ if not containers:
+ raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
+
+ if self._name in containers:
+ self.labels.add_label(label["key"], label["value"])
+
+ def _resolve_image(self, image: str):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(
+ f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
+ )
+ repo = images[image].get("repository", "")
+ tag = images[image].get("tag", "")
+
+ if not repo:
+ raise RenderError(f"Repository not found for image [{image}]")
+ if not tag:
+ raise RenderError(f"Tag not found for image [{image}]")
+
+ return f"{repo}:{tag}"
+
+ def build_image(self, content: list[str | None]):
+ dockerfile = f"FROM {self._image}\n"
+ for line in content:
+ if not line:
+ continue
+ if line.startswith("FROM"):
+ # TODO: This will also block multi-stage builds
+ # We can revisit this later if we need it
+ raise RenderError(
+ "FROM cannot be used in build image. Define the base image when creating the container."
+ )
+ dockerfile += line + "\n"
+
+ self._build_image = dockerfile
+ self._image = get_image_with_hashed_data(self._image, dockerfile)
+
+ def set_pull_policy(self, pull_policy: str):
+ self._pull_policy = valid_pull_policy_or_raise(pull_policy)
+
+ def set_user(self, user: int, group: int):
+ for i in (user, group):
+ if not isinstance(i, int) or i < 0:
+ raise RenderError(f"User/Group [{i}] is not valid")
+ self._user = f"{user}:{group}"
+
+ def add_extra_host(self, host: str, ip: str):
+ self._extra_hosts.add_host(host, ip)
+
+ def add_group(self, group: int | str):
+ if isinstance(group, str):
+ group = str(group).strip()
+ if group.isdigit():
+ raise RenderError(f"Group is a number [{group}] but passed as a string")
+
+ if group in self._group_add:
+ raise RenderError(f"Group [{group}] already added")
+ self._group_add.add(group)
+
+ def get_additional_groups(self) -> list[int | str]:
+ result = []
+ if self.deploy.resources.has_gpus() or self.devices.has_gpus():
+ result.append(44) # video
+ result.append(107) # render
+ return result
+
+ def get_current_groups(self) -> list[str]:
+ result = [str(g) for g in self._group_add]
+ result.extend([str(g) for g in self.get_additional_groups()])
+ return result
+
+ def set_tty(self, enabled: bool = False):
+ self._tty = enabled
+
+ def set_stdin(self, enabled: bool = False):
+ self._stdin_open = enabled
+
+ def set_ipc_mode(self, ipc_mode: str):
+ self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
+
+ def add_device_cgroup_rule(self, dev_grp_rule: str):
+ self._device_cgroup_rules.add_rule(dev_grp_rule)
+
+ def set_init(self, enabled: bool = False):
+ self._init = enabled
+
+ def set_read_only(self, enabled: bool = False):
+ self._read_only = enabled
+
+ def set_hostname(self, hostname: str):
+ self._hostname = hostname
+
+ def set_grace_period(self, grace_period: int):
+ if grace_period < 0:
+ raise RenderError(f"Grace period [{grace_period}] cannot be negative")
+ self._grace_period = grace_period
+
+ def set_privileged(self, enabled: bool = False):
+ self._privileged = enabled
+
+ def clear_caps(self):
+ self._cap_add.clear()
+ self._cap_drop.clear()
+
+ def add_caps(self, caps: list[str]):
+ for c in caps:
+ if c in self._cap_add:
+ raise RenderError(f"Capability [{c}] already added")
+ self._cap_add.add(valid_cap_or_raise(c))
+
+ def add_security_opt(self, opt: str):
+ if opt in self._security_opt:
+ raise RenderError(f"Security Option [{opt}] already added")
+ self._security_opt.add(opt)
+
+ def remove_security_opt(self, opt: str):
+ self._security_opt.remove(opt)
+
+ def set_network_mode(self, mode: str):
+ self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
+
+ def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
+ port_config = port_config or {}
+ dev_config = dev_config or {}
+ # Merge port_config and dev_config (dev_config has precedence)
+ config = port_config | dev_config
+
+ bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
+ # Skip port if its neither published nor exposed
+ if not bind_mode:
+ return
+
+ # Collect port config
+ host_port = config.get("port_number", 0)
+ container_port = config.get("container_port", 0) or host_port
+ protocol = config.get("protocol", "tcp")
+ host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
+ if not isinstance(host_ips, list):
+ raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
+
+ if bind_mode == "published":
+ for host_ip in host_ips:
+ self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip})
+ elif bind_mode == "exposed":
+ self.expose.add_port(container_port, protocol)
+
+ def set_entrypoint(self, entrypoint: list[str]):
+ self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
+
+ def set_command(self, command: list[str]):
+ self._command = [escape_dollar(str(e)) for e in command]
+
+ def add_storage(self, mount_path: str, config: "IxStorage"):
+ self._storage.add(mount_path, config)
+
+ def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
+ self.add_group(999)
+ self._storage._add_docker_socket(read_only, mount_path)
+
+ def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
+ self._storage._add_udev(read_only, mount_path)
+
+ def add_tun_device(self):
+ self.devices._add_tun_device()
+
+ def add_snd_device(self):
+ self.add_group(29)
+ self.devices._add_snd_device()
+
+ def set_shm_size_mb(self, size: int):
+ self._shm_size = size
+
+ # Easily remove devices from the container
+ # Useful in dependencies like postgres and redis
+ # where there is no need to pass devices to them
+ def remove_devices(self):
+ self.deploy.resources.remove_devices()
+ self.devices.remove_devices()
+
+ @property
+ def storage(self):
+ return self._storage
+
+ def render(self) -> dict[str, Any]:
+ if self._network_mode and self.networks:
+ raise RenderError("Cannot set both [network_mode] and [networks]")
+
+ result = {
+ "image": self._image,
+ "platform": "linux/amd64",
+ "tty": self._tty,
+ "stdin_open": self._stdin_open,
+ "restart": self.restart.render(),
+ }
+
+ if self._pull_policy:
+ result["pull_policy"] = self._pull_policy
+
+ if self.healthcheck.has_healthcheck():
+ result["healthcheck"] = self.healthcheck.render()
+
+ if self._hostname:
+ result["hostname"] = self._hostname
+
+ if self._build_image:
+ result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ if self._ipc_mode is not None:
+ result["ipc"] = self._ipc_mode
+
+ if self._device_cgroup_rules.has_rules():
+ result["device_cgroup_rules"] = self._device_cgroup_rules.render()
+
+ if self._extra_hosts.has_hosts():
+ result["extra_hosts"] = self._extra_hosts.render()
+
+ if self._init is not None:
+ result["init"] = self._init
+
+ if self._read_only is not None:
+ result["read_only"] = self._read_only
+
+ if self._grace_period is not None:
+ result["stop_grace_period"] = f"{self._grace_period}s"
+
+ if self._user:
+ result["user"] = self._user
+
+ for g in self.get_additional_groups():
+ self.add_group(g)
+
+ if self._group_add:
+ result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
+
+ if self._shm_size is not None:
+ result["shm_size"] = f"{self._shm_size}M"
+
+ if self._privileged is not None:
+ result["privileged"] = self._privileged
+
+ if self._cap_drop:
+ result["cap_drop"] = sorted(self._cap_drop)
+
+ if self._cap_add:
+ result["cap_add"] = sorted(self._cap_add)
+
+ if self._security_opt:
+ result["security_opt"] = sorted(self._security_opt)
+
+ if self._network_mode:
+ result["network_mode"] = self._network_mode
+
+ if self.sysctls.has_sysctls():
+ result["sysctls"] = self.sysctls.render()
+
+ if self._network_mode != "host":
+ if self.ports.has_ports():
+ result["ports"] = self.ports.render()
+
+ if self.expose.has_ports():
+ result["expose"] = self.expose.render()
+
+ if self._entrypoint:
+ result["entrypoint"] = self._entrypoint
+
+ if self._command:
+ result["command"] = self._command
+
+ if self.devices.has_devices():
+ result["devices"] = self.devices.render()
+
+ if self.deploy.has_deploy():
+ result["deploy"] = self.deploy.render()
+
+ if self.environment.has_variables():
+ result["environment"] = self.environment.render()
+
+ if self.labels.has_labels():
+ result["labels"] = self.labels.render()
+
+ if self.dns.has_dns_nameservers():
+ result["dns"] = self.dns.render_dns_nameservers()
+
+ if self.dns.has_dns_searches():
+ result["dns_search"] = self.dns.render_dns_searches()
+
+ if self.dns.has_dns_opts():
+ result["dns_opt"] = self.dns.render_dns_opts()
+
+ if self.depends.has_dependencies():
+ result["depends_on"] = self.depends.render()
+
+ if self._storage.has_mounts():
+ result["volumes"] = self._storage.render()
+
+ return result
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/depends.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/depends.py
new file mode 100644
index 0000000000..4e057cf085
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/depends.py
@@ -0,0 +1,34 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_depend_condition_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_depend_condition_or_raise
+
+
+class Depends:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dependencies: dict[str, str] = {}
+
+ def add_dependency(self, name: str, condition: str):
+ condition = valid_depend_condition_or_raise(condition)
+ if name in self._dependencies.keys():
+ raise RenderError(f"Dependency [{name}] already added")
+ if name not in self._render_instance.container_names():
+ raise RenderError(
+ f"Dependency [{name}] not found in defined containers. "
+ f"Available containers: [{', '.join(self._render_instance.container_names())}]"
+ )
+ self._dependencies[name] = condition
+
+ def has_dependencies(self):
+ return len(self._dependencies) > 0
+
+ def render(self):
+ return {d: {"condition": c} for d, c in self._dependencies.items()}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deploy.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deploy.py
new file mode 100644
index 0000000000..894dbc643b
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deploy.py
@@ -0,0 +1,24 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .resources import Resources
+except ImportError:
+ from resources import Resources
+
+
+class Deploy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self.resources: Resources = Resources(self._render_instance)
+
+ def has_deploy(self):
+ return self.resources.has_resources()
+
+ def render(self):
+ if self.resources.has_resources():
+ return {"resources": self.resources.render()}
+
+ return {}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps.py
new file mode 100644
index 0000000000..e96849f979
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps.py
@@ -0,0 +1,32 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .deps_postgres import PostgresContainer, PostgresConfig
+ from .deps_redis import RedisContainer, RedisConfig
+ from .deps_mariadb import MariadbContainer, MariadbConfig
+ from .deps_perms import PermsContainer
+except ImportError:
+ from deps_postgres import PostgresContainer, PostgresConfig
+ from deps_redis import RedisContainer, RedisConfig
+ from deps_mariadb import MariadbContainer, MariadbConfig
+ from deps_perms import PermsContainer
+
+
+class Deps:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def perms(self, name: str):
+ return PermsContainer(self._render_instance, name)
+
+ def postgres(self, name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer):
+ return PostgresContainer(self._render_instance, name, image, config, perms_instance)
+
+ def redis(self, name: str, image: str, config: RedisConfig, perms_instance: PermsContainer):
+ return RedisContainer(self._render_instance, name, image, config, perms_instance)
+
+ def mariadb(self, name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer):
+ return MariadbContainer(self._render_instance, name, image, config, perms_instance)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_mariadb.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_mariadb.py
new file mode 100644
index 0000000000..baf863b370
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_mariadb.py
@@ -0,0 +1,65 @@
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class MariadbConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ root_password: NotRequired[str]
+ port: NotRequired[int]
+ auto_upgrade: NotRequired[bool]
+ volume: "IxStorage"
+
+
+class MariadbContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: MariadbConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for mariadb")
+
+ port = valid_port_or_raise(config.get("port") or 3306)
+ root_password = config.get("root_password") or config["password"]
+ auto_upgrade = config.get("auto_upgrade", True)
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(999, 999)
+ c.healthcheck.set_test("mariadb")
+ c.remove_devices()
+
+ c.add_storage("/var/lib/mysql", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_mariadb_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ c.environment.add_env("MARIADB_USER", config["user"])
+ c.environment.add_env("MARIADB_PASSWORD", config["password"])
+ c.environment.add_env("MARIADB_ROOT_PASSWORD", root_password)
+ c.environment.add_env("MARIADB_DATABASE", config["database"])
+ c.environment.add_env("MARIADB_AUTO_UPGRADE", str(auto_upgrade).lower())
+ c.set_command(["--port", str(port)])
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_perms.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_perms.py
new file mode 100644
index 0000000000..cdc5a3820a
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_perms.py
@@ -0,0 +1,252 @@
+import json
+import pathlib
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
+
+
+class PermsContainer:
+ def __init__(self, render_instance: "Render", name: str):
+ self._render_instance = render_instance
+ self._name = name
+ self.actions: set[str] = set()
+ self.parsed_configs: list[dict] = []
+
+ def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ identifier = self.normalize_identifier_for_path(identifier)
+ if identifier in self.actions:
+ raise RenderError(f"Action with id [{identifier}] already used for another permission action")
+
+ parsed_action = self.parse_action(identifier, volume_config, action_config)
+ if parsed_action:
+ self.parsed_configs.append(parsed_action)
+ self.actions.add(identifier)
+
+ def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
+ valid_modes = [
+ "always", # Always set permissions, without checking.
+ "check", # Checks if permissions are correct, and set them if not.
+ ]
+ mode = action_config.get("mode", "check")
+ uid = action_config.get("uid", None)
+ gid = action_config.get("gid", None)
+ chmod = action_config.get("chmod", None)
+ recursive = action_config.get("recursive", False)
+ mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
+ is_temporary = False
+
+ vol_type = volume_config.get("type", "")
+ match vol_type:
+ case "temporary":
+ # If it is a temporary volume, we force auto permissions
+ # and set is_temporary to True, so it will be cleaned up
+ is_temporary = True
+ recursive = True
+ case "volume":
+ if not volume_config.get("volume_config", {}).get("auto_permissions", False):
+ return None
+ case "host_path":
+ host_path_config = volume_config.get("host_path_config", {})
+ # Skip when ACL enabled
+ if host_path_config.get("acl_enable", False):
+ return None
+ if not host_path_config.get("auto_permissions", False):
+ return None
+ case "ix_volume":
+ ix_vol_config = volume_config.get("ix_volume_config", {})
+ # Skip when ACL enabled
+ if ix_vol_config.get("acl_enable", False):
+ return None
+ # For ix_volumes, we default to auto_permissions = True
+ if not ix_vol_config.get("auto_permissions", True):
+ return None
+ case _:
+ # Skip for other types
+ return None
+
+ if mode not in valid_modes:
+ raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
+ if not isinstance(uid, int) or not isinstance(gid, int):
+ raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
+ if chmod is not None:
+ chmod = valid_octal_mode_or_raise(chmod)
+
+ mount_path = valid_fs_path_or_raise(mount_path)
+ return {
+ "mount_path": mount_path,
+ "volume_config": volume_config,
+ "action_data": {
+ "mount_path": mount_path,
+ "is_temporary": is_temporary,
+ "identifier": identifier,
+ "recursive": recursive,
+ "mode": mode,
+ "uid": uid,
+ "gid": gid,
+ "chmod": chmod,
+ },
+ }
+
+ def normalize_identifier_for_path(self, identifier: str):
+ return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
+
+ def has_actions(self):
+ return bool(self.actions)
+
+ def activate(self):
+ if len(self.parsed_configs) != len(self.actions):
+ raise RenderError("Number of actions and parsed configs does not match")
+
+ if not self.has_actions():
+ raise RenderError("No actions added. Check if there are actions before activating")
+
+ # Add the container and set it up
+ c = self._render_instance.add_container(self._name, "python_permissions_image")
+ c.set_user(0, 0)
+ c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
+ c.set_network_mode("none")
+
+ # Don't attach any devices
+ c.remove_devices()
+
+ c.deploy.resources.set_profile("medium")
+ c.restart.set_policy("on-failure", maximum_retry_count=1)
+ c.healthcheck.disable()
+
+ c.set_entrypoint(["python3", "/script/run.py"])
+ script = "#!/usr/bin/env python3\n"
+ script += get_script()
+ c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
+
+ actions_data: list[dict] = []
+ for parsed in self.parsed_configs:
+ c.add_storage(parsed["mount_path"], parsed["volume_config"])
+ actions_data.append(parsed["action_data"])
+
+ actions_data_json = json.dumps(actions_data)
+ c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
+
+
+def get_script():
+ return """
+import os
+import json
+import time
+import shutil
+
+with open("/script/actions.json", "r") as f:
+ actions_data = json.load(f)
+
+if not actions_data:
+ # If this script is called, there should be actions data
+ raise ValueError("No actions data found")
+
+def fix_perms(path, chmod, recursive=False):
+ print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
+ os.chmod(path, int(chmod, 8))
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chmod(os.path.join(root, f), int(chmod, 8))
+ print("Permissions after changes:")
+ print_chmod_stat()
+
+def fix_owner(path, uid, gid, recursive=False):
+ print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
+ os.chown(path, uid, gid)
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ os.chown(os.path.join(root, f), uid, gid)
+ print("Ownership after changes:")
+ print_chown_stat()
+
+def print_chown_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
+
+def print_chmod_stat():
+ curr_stat = os.stat(action["mount_path"])
+ print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
+
+def print_chown_diff(curr_stat, uid, gid):
+ print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
+
+def print_chmod_diff(curr_stat, mode):
+ print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
+
+def perform_action(action):
+ start_time = time.time()
+ print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
+
+ if not os.path.isdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not a directory, skipping...")
+ return
+
+ if action["is_temporary"]:
+ print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
+ for item in os.listdir(action["mount_path"]):
+ item_path = os.path.join(action["mount_path"], item)
+
+ # Exclude the safe directory, where we can use to mount files temporarily
+ if os.path.basename(item_path) == "ix-safe":
+ continue
+ if os.path.isdir(item_path):
+ shutil.rmtree(item_path)
+ else:
+ os.remove(item_path)
+
+ if not action["is_temporary"] and os.listdir(action["mount_path"]):
+ print(f"Path [{action['mount_path']}] is not empty, skipping...")
+ return
+
+ print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
+ curr_stat = os.stat(action["mount_path"])
+ print_chown_diff(curr_stat, action["uid"], action["gid"])
+ print_chmod_diff(curr_stat, action["chmod"])
+ print("---")
+
+ if action["mode"] == "always":
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ return
+
+ elif action["mode"] == "check":
+ if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
+ print("Ownership is incorrect. Fixing...")
+ fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
+ else:
+ print("Ownership is correct. Skipping...")
+
+ if not action["chmod"]:
+ print("Skipping permissions check, chmod is falsy")
+ else:
+ if oct(curr_stat.st_mode)[3:] != action["chmod"]:
+ print("Permissions are incorrect. Fixing...")
+ fix_perms(action["mount_path"], action["chmod"], action["recursive"])
+ else:
+ print("Permissions are correct. Skipping...")
+
+ print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
+ print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
+ print()
+
+if __name__ == "__main__":
+ start_time = time.time()
+ for action in actions_data:
+ perform_action(action)
+ print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
+"""
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_postgres.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_postgres.py
new file mode 100644
index 0000000000..c40e900d07
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_postgres.py
@@ -0,0 +1,279 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise
+
+
+class PostgresConfig(TypedDict):
+ user: str
+ password: str
+ database: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+MAX_POSTGRES_VERSION = 17
+
+
+class PostgresContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: PostgresConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+ self._data_dir = "/var/lib/postgresql/data"
+ self._upgrade_name = f"{self._name}_upgrade"
+ self._upgrade_container = None
+
+ for key in ("user", "password", "database", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for postgres")
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+
+ c.set_user(999, 999)
+ c.healthcheck.set_test("postgres")
+ c.remove_devices()
+ c.add_storage(self._data_dir, config["volume"])
+
+ common_variables = {
+ "POSTGRES_USER": config["user"],
+ "POSTGRES_PASSWORD": config["password"],
+ "POSTGRES_DB": config["database"],
+ "POSTGRES_PORT": port,
+ }
+
+ for k, v in common_variables.items():
+ c.environment.add_env(k, v)
+
+ perms_instance.add_or_skip_action(
+ f"{self._name}_postgres_data", config["volume"], {"uid": 999, "gid": 999, "mode": "check"}
+ )
+
+ repo = self._get_repo(image)
+ # eg we don't want to handle upgrades of pg_vector at the moment
+ if repo == "postgres":
+ target_major_version = self._get_target_version(image)
+ upg = self._render_instance.add_container(self._upgrade_name, image)
+ upg.build_image(get_build_manifest())
+ upg.set_entrypoint(["/bin/bash", "-c", "/upgrade.sh"])
+ upg.configs.add("pg_container_upgrade.sh", get_upgrade_script(), "/upgrade.sh", "0755")
+ upg.restart.set_policy("on-failure", 1)
+ upg.set_user(999, 999)
+ upg.healthcheck.disable()
+ upg.remove_devices()
+ upg.add_storage(self._data_dir, config["volume"])
+ for k, v in common_variables.items():
+ upg.environment.add_env(k, v)
+
+ upg.environment.add_env("TARGET_VERSION", target_major_version)
+ upg.environment.add_env("DATA_DIR", self._data_dir)
+
+ self._upgrade_container = upg
+
+ c.depends.add_dependency(self._upgrade_name, "service_completed_successfully")
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ @property
+ def container(self):
+ return self._container
+
+ def add_dependency(self, container_name: str, condition: str):
+ self._container.depends.add_dependency(container_name, condition)
+ if self._upgrade_container:
+ self._upgrade_container.depends.add_dependency(container_name, condition)
+
+ def _get_port(self):
+ return self._config.get("port") or 5432
+
+ def _get_repo(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ repo = images[image].get("repository", "")
+ if not repo:
+ raise RenderError("Could not determine repo")
+ return repo
+
+ def _get_target_version(self, image):
+ images = self._render_instance.values["images"]
+ if image not in images:
+ raise RenderError(f"Image [{image}] not found in values. Available images: [{', '.join(images.keys())}]")
+ tag = images[image].get("tag", "")
+ tag = str(tag) # Account for tags like 16.6
+ target_major_version = tag.split(".")[0]
+
+ try:
+ target_major_version = int(target_major_version)
+ except ValueError:
+ raise RenderError(f"Could not determine target major version from tag [{tag}]")
+
+ if target_major_version > MAX_POSTGRES_VERSION:
+ raise RenderError(f"Postgres version [{target_major_version}] is not supported")
+
+ return target_major_version
+
+ def get_url(self, variant: str):
+ user = urllib.parse.quote_plus(self._config["user"])
+ password = urllib.parse.quote_plus(self._config["password"])
+ creds = f"{user}:{password}"
+ addr = f"{self._name}:{self._get_port()}"
+ db = self._config["database"]
+
+ match variant:
+ case "postgres":
+ return f"postgres://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql":
+ return f"postgresql://{creds}@{addr}/{db}?sslmode=disable"
+ case "postgresql_no_creds":
+ return f"postgresql://{addr}/{db}?sslmode=disable"
+ case "host_port":
+ return addr
+ case _:
+ raise RenderError(f"Expected [variant] to be one of [postgres, postgresql], got [{variant}]")
+
+
+def get_build_manifest() -> list[str | None]:
+ return [
+ f"RUN apt-get update && apt-get install -y {' '.join(get_upgrade_packages())}",
+ "WORKDIR /tmp",
+ ]
+
+
+def get_upgrade_packages():
+ return [
+ "rsync",
+ "postgresql-13",
+ "postgresql-14",
+ "postgresql-15",
+ "postgresql-16",
+ ]
+
+
+def get_upgrade_script():
+ return """
+#!/bin/bash
+set -euo pipefail
+
+get_bin_path() {
+ local version=$1
+ echo "/usr/lib/postgresql/$version/bin"
+}
+
+log() {
+ echo "[ix-postgres-upgrade] - [$(date +'%Y-%m-%d %H:%M:%S')] - $1"
+}
+
+check_writable() {
+ local path=$1
+ if [ ! -w "$path" ]; then
+ log "$path is not writable"
+ exit 1
+ fi
+}
+
+check_writable "$DATA_DIR"
+
+# Don't do anything if its a fresh install.
+if [ ! -f "$DATA_DIR/PG_VERSION" ]; then
+ log "File $DATA_DIR/PG_VERSION does not exist. Assuming this is a fresh install."
+ exit 0
+fi
+
+# Don't do anything if we're already at the target version.
+OLD_VERSION=$(cat "$DATA_DIR/PG_VERSION")
+log "Current version: $OLD_VERSION"
+log "Target version: $TARGET_VERSION"
+if [ "$OLD_VERSION" -eq "$TARGET_VERSION" ]; then
+ log "Already at target version $TARGET_VERSION"
+ exit 0
+fi
+
+# Fail if we're downgrading.
+if [ "$OLD_VERSION" -gt "$TARGET_VERSION" ]; then
+ log "Cannot downgrade from $OLD_VERSION to $TARGET_VERSION"
+ exit 1
+fi
+
+export OLD_PG_BINARY=$(get_bin_path "$OLD_VERSION")
+if [ ! -f "$OLD_PG_BINARY/pg_upgrade" ]; then
+ log "File $OLD_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_PG_BINARY=$(get_bin_path "$TARGET_VERSION")
+if [ ! -f "$NEW_PG_BINARY/pg_upgrade" ]; then
+ log "File $NEW_PG_BINARY/pg_upgrade does not exist."
+ exit 1
+fi
+
+export NEW_DATA_DIR="/tmp/new-data-dir"
+if [ -d "$NEW_DATA_DIR" ]; then
+ log "Directory $NEW_DATA_DIR already exists."
+ exit 1
+fi
+
+export PGUSER="$POSTGRES_USER"
+log "Creating new data dir and initializing..."
+PGDATA="$NEW_DATA_DIR" eval "initdb --username=$POSTGRES_USER --pwfile=<(echo $POSTGRES_PASSWORD)"
+
+timestamp=$(date +%Y%m%d%H%M%S)
+backup_name="backup-$timestamp-$OLD_VERSION-$TARGET_VERSION.tar.gz"
+log "Backing up $DATA_DIR to $NEW_DATA_DIR/$backup_name"
+tar -czf "$NEW_DATA_DIR/$backup_name" "$DATA_DIR"
+
+log "Using old pg_upgrade [$OLD_PG_BINARY/pg_upgrade]"
+log "Using new pg_upgrade [$NEW_PG_BINARY/pg_upgrade]"
+log "Checking upgrade compatibility of $OLD_VERSION to $TARGET_VERSION..."
+
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql \
+ --check
+
+log "Compatibility check passed."
+
+log "Upgrading from $OLD_VERSION to $TARGET_VERSION..."
+"$NEW_PG_BINARY"/pg_upgrade \
+ --old-bindir="$OLD_PG_BINARY" \
+ --new-bindir="$NEW_PG_BINARY" \
+ --old-datadir="$DATA_DIR" \
+ --new-datadir="$NEW_DATA_DIR" \
+ --socketdir /var/run/postgresql
+
+log "Upgrade complete."
+
+log "Copying old pg_hba.conf to new pg_hba.conf"
+# We need to carry this over otherwise
+cp "$DATA_DIR/pg_hba.conf" "$NEW_DATA_DIR/pg_hba.conf"
+
+log "Replacing contents of $DATA_DIR with contents of $NEW_DATA_DIR (including the backup)."
+rsync --archive --delete "$NEW_DATA_DIR/" "$DATA_DIR/"
+
+log "Removing $NEW_DATA_DIR."
+rm -rf "$NEW_DATA_DIR"
+
+log "Done."
+"""
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_redis.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_redis.py
new file mode 100644
index 0000000000..d49ebe7683
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/deps_redis.py
@@ -0,0 +1,71 @@
+import urllib.parse
+from typing import TYPE_CHECKING, TypedDict, NotRequired
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .deps_perms import PermsContainer
+ from .validations import valid_port_or_raise, valid_redis_password_or_raise
+except ImportError:
+ from error import RenderError
+ from deps_perms import PermsContainer
+ from validations import valid_port_or_raise, valid_redis_password_or_raise
+
+
+class RedisConfig(TypedDict):
+ password: str
+ port: NotRequired[int]
+ volume: "IxStorage"
+
+
+class RedisContainer:
+ def __init__(
+ self, render_instance: "Render", name: str, image: str, config: RedisConfig, perms_instance: PermsContainer
+ ):
+ self._render_instance = render_instance
+ self._name = name
+ self._config = config
+
+ for key in ("password", "volume"):
+ if key not in config:
+ raise RenderError(f"Expected [{key}] to be set for redis")
+
+ valid_redis_password_or_raise(config["password"])
+
+ port = valid_port_or_raise(self._get_port())
+
+ c = self._render_instance.add_container(name, image)
+ c.set_user(1001, 0)
+ c.healthcheck.set_test("redis")
+ c.remove_devices()
+
+ c.add_storage("/bitnami/redis/data", config["volume"])
+ perms_instance.add_or_skip_action(
+ f"{self._name}_redis_data", config["volume"], {"uid": 1001, "gid": 0, "mode": "check"}
+ )
+
+ c.environment.add_env("ALLOW_EMPTY_PASSWORD", "no")
+ c.environment.add_env("REDIS_PASSWORD", config["password"])
+ c.environment.add_env("REDIS_PORT_NUMBER", port)
+
+ # Store container for further configuration
+ # For example: c.depends.add_dependency("other_container", "service_started")
+ self._container = c
+
+ def _get_port(self):
+ return self._config.get("port") or 6379
+
+ def get_url(self, variant: str):
+ addr = f"{self._name}:{self._get_port()}"
+ password = urllib.parse.quote_plus(self._config["password"])
+
+ match variant:
+ case "redis":
+ return f"redis://default:{password}@{addr}"
+
+ @property
+ def container(self):
+ return self._container
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device.py
new file mode 100644
index 0000000000..bfe97097cb
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device.py
@@ -0,0 +1,31 @@
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise, allowed_device_or_raise, valid_cgroup_perm_or_raise
+
+
+class Device:
+ def __init__(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ hd = valid_fs_path_or_raise(host_device.rstrip("/"))
+ cd = valid_fs_path_or_raise(container_device.rstrip("/"))
+ if not hd or not cd:
+ raise RenderError(
+ "Expected [host_device] and [container_device] to be set. "
+ f"Got host_device [{host_device}] and container_device [{container_device}]"
+ )
+
+ cgroup_perm = valid_cgroup_perm_or_raise(cgroup_perm)
+ if not allow_disallowed:
+ hd = allowed_device_or_raise(hd)
+
+ self.cgroup_perm: str = cgroup_perm
+ self.host_device: str = hd
+ self.container_device: str = cd
+
+ def render(self):
+ result = f"{self.host_device}:{self.container_device}"
+ if self.cgroup_perm:
+ result += f":{self.cgroup_perm}"
+ return result
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device_cgroup_rules.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device_cgroup_rules.py
new file mode 100644
index 0000000000..dcccfee773
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/device_cgroup_rules.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_device_cgroup_rule_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_device_cgroup_rule_or_raise
+
+
+class DeviceCGroupRule:
+ def __init__(self, rule: str):
+ rule = valid_device_cgroup_rule_or_raise(rule)
+ parts = rule.split(" ")
+ major, minor = parts[1].split(":")
+
+ self._type = parts[0]
+ self._major = major
+ self._minor = minor
+ self._permissions = parts[2]
+
+ def get_key(self):
+ return f"{self._type}_{self._major}_{self._minor}"
+
+ def render(self):
+ return f"{self._type} {self._major}:{self._minor} {self._permissions}"
+
+
+class DeviceCGroupRules:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._rules: set[DeviceCGroupRule] = set()
+ self._track_rule_combos: set[str] = set()
+
+ def add_rule(self, rule: str):
+ dev_group_rule = DeviceCGroupRule(rule)
+ if dev_group_rule in self._rules:
+ raise RenderError(f"Device Group Rule [{rule}] already added")
+
+ rule_key = dev_group_rule.get_key()
+ if rule_key in self._track_rule_combos:
+ raise RenderError(f"Device Group Rule [{rule}] has already been added for this device group")
+
+ self._rules.add(dev_group_rule)
+ self._track_rule_combos.add(rule_key)
+
+ def has_rules(self):
+ return len(self._rules) > 0
+
+ def render(self):
+ return sorted([rule.render() for rule in self._rules])
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/devices.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/devices.py
new file mode 100644
index 0000000000..168e98d032
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/devices.py
@@ -0,0 +1,71 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .device import Device
+except ImportError:
+ from error import RenderError
+ from device import Device
+
+
+class Devices:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._devices: set[Device] = set()
+
+ # Tracks all container device paths to make sure they are not duplicated
+ self._container_device_paths: set[str] = set()
+ # Scan values for devices we should automatically add
+ # for example /dev/dri for gpus
+ self._auto_add_devices_from_values()
+
+ def _auto_add_devices_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+
+ if resources.get("gpus", {}).get("use_all_gpus", False):
+ self.add_device("/dev/dri", "/dev/dri", allow_disallowed=True)
+ if resources["gpus"].get("kfd_device_exists", False):
+ self.add_device("/dev/kfd", "/dev/kfd", allow_disallowed=True) # AMD ROCm
+
+ def add_device(self, host_device: str, container_device: str, cgroup_perm: str = "", allow_disallowed=False):
+ # Host device can be mapped to multiple container devices,
+ # so we only make sure container devices are not duplicated
+ if container_device in self._container_device_paths:
+ raise RenderError(f"Device with container path [{container_device}] already added")
+
+ self._devices.add(Device(host_device, container_device, cgroup_perm, allow_disallowed))
+ self._container_device_paths.add(container_device)
+
+ def add_usb_bus(self):
+ self.add_device("/dev/bus/usb", "/dev/bus/usb", allow_disallowed=True)
+
+ def _add_snd_device(self):
+ self.add_device("/dev/snd", "/dev/snd", allow_disallowed=True)
+
+ def _add_tun_device(self):
+ self.add_device("/dev/net/tun", "/dev/net/tun", allow_disallowed=True)
+
+ def has_devices(self):
+ return len(self._devices) > 0
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._devices.clear()
+ self._container_device_paths.clear()
+
+ # Check if there are any gpu devices
+ # Used to determine if we should add groups
+ # like 'video' to the container
+ def has_gpus(self):
+ for d in self._devices:
+ if d.host_device == "/dev/dri":
+ return True
+ return False
+
+ def render(self) -> list[str]:
+ return sorted([d.render() for d in self._devices])
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/dns.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/dns.py
new file mode 100644
index 0000000000..d3ae7b19fa
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/dns.py
@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import allowed_dns_opt_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import allowed_dns_opt_or_raise
+
+
+class Dns:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._dns_options: set[str] = set()
+ self._dns_searches: set[str] = set()
+ self._dns_nameservers: set[str] = set()
+
+ self._auto_add_dns_opts_from_values()
+ self._auto_add_dns_searches_from_values()
+ self._auto_add_dns_nameservers_from_values()
+
+ def _get_dns_opt_keys(self):
+ return [self._get_key_from_opt(opt) for opt in self._dns_options]
+
+ def _get_key_from_opt(self, opt):
+ return opt.split(":")[0]
+
+ def _auto_add_dns_opts_from_values(self):
+ values = self._render_instance.values
+ for dns_opt in values.get("network", {}).get("dns_opts", []):
+ self.add_dns_opt(dns_opt)
+
+ def _auto_add_dns_searches_from_values(self):
+ values = self._render_instance.values
+ for dns_search in values.get("network", {}).get("dns_searches", []):
+ self.add_dns_search(dns_search)
+
+ def _auto_add_dns_nameservers_from_values(self):
+ values = self._render_instance.values
+ for dns_nameserver in values.get("network", {}).get("dns_nameservers", []):
+ self.add_dns_nameserver(dns_nameserver)
+
+ def add_dns_search(self, dns_search):
+ if dns_search in self._dns_searches:
+ raise RenderError(f"DNS Search [{dns_search}] already added")
+ self._dns_searches.add(dns_search)
+
+ def add_dns_nameserver(self, dns_nameserver):
+ if dns_nameserver in self._dns_nameservers:
+ raise RenderError(f"DNS Nameserver [{dns_nameserver}] already added")
+ self._dns_nameservers.add(dns_nameserver)
+
+ def add_dns_opt(self, dns_opt):
+ # eg attempts:3
+ key = allowed_dns_opt_or_raise(self._get_key_from_opt(dns_opt))
+ if key in self._get_dns_opt_keys():
+ raise RenderError(f"DNS Option [{key}] already added")
+ self._dns_options.add(dns_opt)
+
+ def has_dns_opts(self):
+ return len(self._dns_options) > 0
+
+ def has_dns_searches(self):
+ return len(self._dns_searches) > 0
+
+ def has_dns_nameservers(self):
+ return len(self._dns_nameservers) > 0
+
+ def render_dns_searches(self):
+ return sorted(self._dns_searches)
+
+ def render_dns_opts(self):
+ return sorted(self._dns_options)
+
+ def render_dns_nameservers(self):
+ return sorted(self._dns_nameservers)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/environment.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/environment.py
new file mode 100644
index 0000000000..056763ea80
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/environment.py
@@ -0,0 +1,112 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .resources import Resources
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from resources import Resources
+
+
+class Environment:
+ def __init__(self, render_instance: "Render", resources: Resources):
+ self._render_instance = render_instance
+ self._resources = resources
+ # Stores variables that user defined
+ self._user_vars: dict[str, Any] = {}
+ # Stores variables that are automatically added (based on values)
+ self._auto_variables: dict[str, Any] = {}
+ # Stores variables that are added by the application developer
+ self._app_dev_variables: dict[str, Any] = {}
+
+ self._skip_generic_variables: bool = render_instance.values.get("skip_generic_variables", False)
+
+ self._auto_add_variables_from_values()
+
+ def _auto_add_variables_from_values(self):
+ if not self._skip_generic_variables:
+ self._add_generic_variables()
+ self._add_nvidia_variables()
+
+ def _add_generic_variables(self):
+ self._auto_variables["TZ"] = self._render_instance.values.get("TZ", "Etc/UTC")
+ self._auto_variables["UMASK"] = self._render_instance.values.get("UMASK", "002")
+ self._auto_variables["UMASK_SET"] = self._render_instance.values.get("UMASK", "002")
+
+ run_as = self._render_instance.values.get("run_as", {})
+ user = run_as.get("user")
+ group = run_as.get("group")
+ if user:
+ self._auto_variables["PUID"] = user
+ self._auto_variables["UID"] = user
+ self._auto_variables["USER_ID"] = user
+ if group:
+ self._auto_variables["PGID"] = group
+ self._auto_variables["GID"] = group
+ self._auto_variables["GROUP_ID"] = group
+
+ def _add_nvidia_variables(self):
+ if self._resources._nvidia_ids:
+ self._auto_variables["NVIDIA_DRIVER_CAPABILITIES"] = "all"
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(sorted(self._resources._nvidia_ids))
+ else:
+ self._auto_variables["NVIDIA_VISIBLE_DEVICES"] = "void"
+
+ def _format_value(self, v: Any) -> str:
+ value = str(v)
+
+ # str(bool) returns "True" or "False",
+ # but we want "true" or "false"
+ if isinstance(v, bool):
+ value = value.lower()
+ return value
+
+ def add_env(self, name: str, value: Any):
+ if not name:
+ raise RenderError(f"Environment variable name cannot be empty. [{name}]")
+ if name in self._app_dev_variables.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{name}] in application developer environment variables."
+ )
+ self._app_dev_variables[name] = value
+
+ def add_user_envs(self, user_env: list[dict]):
+ for item in user_env:
+ if not item.get("name"):
+ raise RenderError(f"Environment variable name cannot be empty. [{item}]")
+ if item["name"] in self._user_vars.keys():
+ raise RenderError(
+ f"Found duplicate environment variable [{item['name']}] in user environment variables."
+ )
+ self._user_vars[item["name"]] = item.get("value")
+
+ def has_variables(self):
+ return len(self._auto_variables) > 0 or len(self._user_vars) > 0 or len(self._app_dev_variables) > 0
+
+ def render(self):
+ result: dict[str, str] = {}
+
+ # Add envs from auto variables
+ result.update({k: self._format_value(v) for k, v in self._auto_variables.items()})
+
+ # Track defined keys for faster lookup
+ defined_keys = set(result.keys())
+
+ # Add envs from application developer (prohibit overwriting auto variables)
+ for k, v in self._app_dev_variables.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined automatically from the library.")
+ result[k] = self._format_value(v)
+ defined_keys.add(k)
+
+ # Add envs from user (prohibit overwriting app developer envs and auto variables)
+ for k, v in self._user_vars.items():
+ if k in defined_keys:
+ raise RenderError(f"Environment variable [{k}] is already defined from the application developer.")
+ result[k] = self._format_value(v)
+
+ return {k: escape_dollar(v) for k, v in result.items()}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/error.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/error.py
new file mode 100644
index 0000000000..aef48d3b02
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/error.py
@@ -0,0 +1,4 @@
+class RenderError(Exception):
+ """Base class for exceptions in this module."""
+
+ pass
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/expose.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/expose.py
new file mode 100644
index 0000000000..a3ac0aec59
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/expose.py
@@ -0,0 +1,31 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_port_or_raise, valid_port_protocol_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_port_or_raise, valid_port_protocol_or_raise
+
+
+class Expose:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: set[str] = set()
+
+ def add_port(self, port: int, protocol: str = "tcp"):
+ port = valid_port_or_raise(port)
+ protocol = valid_port_protocol_or_raise(protocol)
+ key = f"{port}/{protocol}"
+ if key in self._ports:
+ raise RenderError(f"Exposed port [{port}/{protocol}] already added")
+ self._ports.add(key)
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ return sorted(self._ports)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/extra_hosts.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/extra_hosts.py
new file mode 100644
index 0000000000..eaad3bed26
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/extra_hosts.py
@@ -0,0 +1,33 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+
+class ExtraHosts:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._extra_hosts: dict[str, str] = {}
+
+ def add_host(self, host: str, ip: str):
+ if not ip == "host-gateway":
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ raise RenderError(f"Invalid IP address [{ip}] for host [{host}]")
+
+ if host in self._extra_hosts:
+ raise RenderError(f"Host [{host}] already added with [{self._extra_hosts[host]}]")
+ self._extra_hosts[host] = ip
+
+ def has_hosts(self):
+ return len(self._extra_hosts) > 0
+
+ def render(self):
+ return {host: ip for host, ip in self._extra_hosts.items()}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/formatter.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/formatter.py
new file mode 100644
index 0000000000..24e882f47a
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/formatter.py
@@ -0,0 +1,26 @@
+import json
+import hashlib
+
+
+def escape_dollar(text: str) -> str:
+ return text.replace("$", "$$")
+
+
+def get_hashed_name_for_volume(prefix: str, config: dict):
+ config_hash = hashlib.sha256(json.dumps(config).encode("utf-8")).hexdigest()
+ return f"{prefix}_{config_hash}"
+
+
+def get_hash_with_prefix(prefix: str, data: str):
+ return f"{prefix}_{hashlib.sha256(data.encode('utf-8')).hexdigest()}"
+
+
+def merge_dicts_no_overwrite(dict1, dict2):
+ overlapping_keys = dict1.keys() & dict2.keys()
+ if overlapping_keys:
+ raise ValueError(f"Merging of dicts failed. Overlapping keys: {overlapping_keys}")
+ return {**dict1, **dict2}
+
+
+def get_image_with_hashed_data(image: str, data: str):
+ return get_hash_with_prefix(f"ix-{image}", data)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/functions.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/functions.py
new file mode 100644
index 0000000000..02c9cd4708
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/functions.py
@@ -0,0 +1,149 @@
+import re
+import copy
+import bcrypt
+import secrets
+from base64 import b64encode
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .volume_sources import HostPathSource, IxVolumeSource
+except ImportError:
+ from error import RenderError
+ from volume_sources import HostPathSource, IxVolumeSource
+
+
+class Functions:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+
+ def _bcrypt_hash(self, password):
+ hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
+ return hashed
+
+ def _htpasswd(self, username, password):
+ hashed = self._bcrypt_hash(password)
+ return username + ":" + hashed
+
+ def _secure_string(self, length):
+ return secrets.token_urlsafe(length)[:length]
+
+ def _basic_auth(self, username, password):
+ return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
+
+ def _basic_auth_header(self, username, password):
+ return f"Basic {self._basic_auth(username, password)}"
+
+ def _fail(self, message):
+ raise RenderError(message)
+
+ def _camel_case(self, string):
+ return string.title()
+
+ def _auto_cast(self, value):
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return float(value)
+ except ValueError:
+ pass
+
+ if value.lower() in ["true", "false"]:
+ return value.lower() == "true"
+
+ return value
+
+ def _match_regex(self, value, regex):
+ if not re.match(regex, value):
+ return False
+ return True
+
+ def _must_match_regex(self, value, regex):
+ if not self._match_regex(value, regex):
+ raise RenderError(f"Expected [{value}] to match [{regex}]")
+ return value
+
+ def _is_boolean(self, string):
+ return string.lower() in ["true", "false"]
+
+ def _is_number(self, string):
+ try:
+ float(string)
+ return True
+ except ValueError:
+ return False
+
+ def _copy_dict(self, dict):
+ return copy.deepcopy(dict)
+
+ def _merge_dicts(self, *dicts):
+ merged_dict = {}
+ for dictionary in dicts:
+ merged_dict.update(dictionary)
+ return merged_dict
+
+ def _disallow_chars(self, string: str, chars: list[str], key: str):
+ for char in chars:
+ if char in string:
+ raise RenderError(f"Disallowed character [{char}] in [{key}]")
+ return string
+
+ def _or_default(self, value, default):
+ if not value:
+ return default
+ return value
+
+ def _temp_config(self, name):
+ if not name:
+ raise RenderError("Expected [name] to be set when calling [temp_config].")
+ return {"type": "temporary", "volume_config": {"volume_name": name}}
+
+ def _get_host_path(self, storage):
+ source_type = storage.get("type", "")
+ if not source_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match source_type:
+ case "host_path":
+ mount_config = storage.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ host_source = HostPathSource(self._render_instance, mount_config).get()
+ return host_source
+ case "ix_volume":
+ mount_config = storage.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ ix_source = IxVolumeSource(self._render_instance, mount_config).get()
+ return ix_source
+ case _:
+ raise RenderError(f"Storage type [{source_type}] does not support host path.")
+
+ def func_map(self):
+ # TODO: Check what is no longer used and remove
+ return {
+ "auto_cast": self._auto_cast,
+ "basic_auth_header": self._basic_auth_header,
+ "basic_auth": self._basic_auth,
+ "bcrypt_hash": self._bcrypt_hash,
+ "camel_case": self._camel_case,
+ "copy_dict": self._copy_dict,
+ "fail": self._fail,
+ "htpasswd": self._htpasswd,
+ "is_boolean": self._is_boolean,
+ "is_number": self._is_number,
+ "match_regex": self._match_regex,
+ "merge_dicts": self._merge_dicts,
+ "must_match_regex": self._must_match_regex,
+ "secure_string": self._secure_string,
+ "disallow_chars": self._disallow_chars,
+ "get_host_path": self._get_host_path,
+ "or_default": self._or_default,
+ "temp_config": self._temp_config,
+ }
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/healthcheck.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/healthcheck.py
new file mode 100644
index 0000000000..0805329284
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/healthcheck.py
@@ -0,0 +1,203 @@
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_http_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_http_path_or_raise
+
+
+class Healthcheck:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._test: str | list[str] = ""
+ self._interval_sec: int = 10
+ self._timeout_sec: int = 5
+ self._retries: int = 30
+ self._start_period_sec: int = 10
+ self._disabled: bool = False
+ self._use_built_in: bool = False
+
+ def _get_test(self):
+ if isinstance(self._test, str):
+ return escape_dollar(self._test)
+
+ return [escape_dollar(t) for t in self._test]
+
+ def disable(self):
+ self._disabled = True
+
+ def use_built_in(self):
+ self._use_built_in = True
+
+ def set_custom_test(self, test: str | list[str]):
+ if self._disabled:
+ raise RenderError("Cannot set custom test when healthcheck is disabled")
+ self._test = test
+
+ def set_test(self, variant: str, config: dict | None = None):
+ config = config or {}
+ self.set_custom_test(test_mapping(variant, config))
+
+ def set_interval(self, interval: int):
+ self._interval_sec = interval
+
+ def set_timeout(self, timeout: int):
+ self._timeout_sec = timeout
+
+ def set_retries(self, retries: int):
+ self._retries = retries
+
+ def set_start_period(self, start_period: int):
+ self._start_period_sec = start_period
+
+ def has_healthcheck(self):
+ return not self._use_built_in
+
+ def render(self):
+ if self._use_built_in:
+ return RenderError("Should not be called when built in healthcheck is used")
+
+ if self._disabled:
+ return {"disable": True}
+
+ if not self._test:
+ raise RenderError("Healthcheck test is not set")
+
+ return {
+ "test": self._get_test(),
+ "interval": f"{self._interval_sec}s",
+ "timeout": f"{self._timeout_sec}s",
+ "retries": self._retries,
+ "start_period": f"{self._start_period_sec}s",
+ }
+
+
+def test_mapping(variant: str, config: dict | None = None) -> str:
+ config = config or {}
+ tests = {
+ "curl": curl_test,
+ "wget": wget_test,
+ "http": http_test,
+ "netcat": netcat_test,
+ "tcp": tcp_test,
+ "redis": redis_test,
+ "postgres": postgres_test,
+ "mariadb": mariadb_test,
+ }
+
+ if variant not in tests:
+ raise RenderError(f"Test variant [{variant}] is not valid. Valid options are: [{', '.join(tests.keys())}]")
+
+ return tests[variant](config)
+
+
+def get_key(config: dict, key: str, default: Any, required: bool):
+ if not config.get(key):
+ if not required:
+ return default
+ raise RenderError(f"Expected [{key}] to be set")
+ return config[key]
+
+
+def curl_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--insecure")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for curl test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "curl --silent --output /dev/null --show-error --fail"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def wget_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ scheme = get_key(config, "scheme", "http", False)
+ host = get_key(config, "host", "127.0.0.1", False)
+ headers = get_key(config, "headers", [], False)
+
+ opts = []
+ if scheme == "https":
+ opts.append("--no-check-certificate")
+
+ for header in headers:
+ if not header[0] or not header[1]:
+ raise RenderError("Expected [header] to be a list of two items for wget test")
+ opts.append(f'--header "{header[0]}: {header[1]}"')
+
+ cmd = "wget --spider --quiet"
+ if opts:
+ cmd += f" {' '.join(opts)}"
+ cmd += f" {scheme}://{host}:{port}{path}"
+ return cmd
+
+
+def http_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ path = valid_http_path_or_raise(get_key(config, "path", "/", False))
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"""/bin/bash -c 'exec {{hc_fd}}<>/dev/tcp/{host}/{port} && echo -e "GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n" >&${{hc_fd}} && cat <&${{hc_fd}} | grep "HTTP" | grep -q "200"'""" # noqa
+
+
+def netcat_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"nc -z -w 1 {host} {port}"
+
+
+def tcp_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", None, True)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"timeout 1 bash -c 'cat < /dev/null > /dev/tcp/{host}/{port}'"
+
+
+def redis_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 6379, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"redis-cli -h {host} -p {port} -a $REDIS_PASSWORD ping | grep -q PONG"
+
+
+def postgres_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 5432, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"pg_isready -h {host} -p {port} -U $POSTGRES_USER -d $POSTGRES_DB"
+
+
+def mariadb_test(config: dict) -> str:
+ config = config or {}
+ port = get_key(config, "port", 3306, False)
+ host = get_key(config, "host", "127.0.0.1", False)
+
+ return f"mariadb-admin --user=root --host={host} --port={port} --password=$MARIADB_ROOT_PASSWORD ping"
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/labels.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/labels.py
new file mode 100644
index 0000000000..f1e667ba00
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/labels.py
@@ -0,0 +1,37 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+
+
+class Labels:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._labels: dict[str, str] = {}
+
+ def add_label(self, key: str, value: str):
+ if not key:
+ raise RenderError("Labels must have a key")
+
+ if key.startswith("com.docker.compose"):
+ raise RenderError(f"Label [{key}] cannot start with [com.docker.compose] as it is reserved")
+
+ if key in self._labels.keys():
+ raise RenderError(f"Label [{key}] already added")
+
+ self._labels[key] = escape_dollar(str(value))
+
+ def has_labels(self) -> bool:
+ return bool(self._labels)
+
+ def render(self) -> dict[str, str]:
+ if not self.has_labels():
+ return {}
+ return {label: value for label, value in sorted(self._labels.items())}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/notes.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/notes.py
new file mode 100644
index 0000000000..eb8d9f37fc
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/notes.py
@@ -0,0 +1,76 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+class Notes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._app_name: str = ""
+ self._app_train: str = ""
+ self._warnings: list[str] = []
+ self._deprecations: list[str] = []
+ self._header: str = ""
+ self._body: str = ""
+ self._footer: str = ""
+
+ self._auto_set_app_name()
+ self._auto_set_app_train()
+ self._auto_set_header()
+ self._auto_set_footer()
+
+ def _is_enterprise_train(self):
+ if self._app_train == "enterprise":
+ return True
+
+ def _auto_set_app_name(self):
+ app_name = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("title", "")
+ self._app_name = app_name or ""
+
+ def _auto_set_app_train(self):
+ app_train = self._render_instance.values.get("ix_context", {}).get("app_metadata", {}).get("train", "")
+ self._app_train = app_train or ""
+
+ def _auto_set_header(self):
+ self._header = f"# {self._app_name}\n\n"
+
+ def _auto_set_footer(self):
+ url = "https://github.com/truenas/apps"
+ if self._is_enterprise_train():
+ url = "https://ixsystems.atlassian.net"
+ footer = "## Bug Reports and Feature Requests\n\n"
+ footer += "If you find a bug in this app or have an idea for a new feature, please file an issue at\n"
+ footer += f"{url}\n\n"
+ self._footer = footer
+
+ def add_warning(self, warning: str):
+ self._warnings.append(warning)
+
+ def add_deprecation(self, deprecation: str):
+ self._deprecations.append(deprecation)
+
+ def set_body(self, body: str):
+ self._body = body
+
+ def render(self):
+ result = self._header
+
+ if self._warnings:
+ result += "## Warnings\n\n"
+ for warning in self._warnings:
+ result += f"- {warning}\n"
+ result += "\n"
+
+ if self._deprecations:
+ result += "## Deprecations\n\n"
+ for deprecation in self._deprecations:
+ result += f"- {deprecation}\n"
+ result += "\n"
+
+ if self._body:
+ result += self._body.strip() + "\n\n"
+
+ result += self._footer
+
+ return result
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portal.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portal.py
new file mode 100644
index 0000000000..cf47163439
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portal.py
@@ -0,0 +1,22 @@
+try:
+ from .validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+except ImportError:
+ from validations import valid_portal_scheme_or_raise, valid_http_path_or_raise, valid_port_or_raise
+
+
+class Portal:
+ def __init__(self, name: str, config: dict):
+ self._name = name
+ self._scheme = valid_portal_scheme_or_raise(config.get("scheme", "http"))
+ self._host = config.get("host", "0.0.0.0") or "0.0.0.0"
+ self._port = valid_port_or_raise(config.get("port", 0))
+ self._path = valid_http_path_or_raise(config.get("path", "/"))
+
+ def render(self):
+ return {
+ "name": self._name,
+ "scheme": self._scheme,
+ "host": self._host,
+ "port": self._port,
+ "path": self._path,
+ }
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portals.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portals.py
new file mode 100644
index 0000000000..e106d231e6
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/portals.py
@@ -0,0 +1,28 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .portal import Portal
+except ImportError:
+ from error import RenderError
+ from portal import Portal
+
+
+class Portals:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._portals: set[Portal] = set()
+
+ def add_portal(self, config: dict):
+ name = config.get("name", "Web UI")
+
+ if name in [p._name for p in self._portals]:
+ raise RenderError(f"Portal [{name}] already added")
+
+ self._portals.add(Portal(name, config))
+
+ def render(self):
+ return [p.render() for _, p in sorted([(p._name, p) for p in self._portals])]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/ports.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/ports.py
new file mode 100644
index 0000000000..e571232ac9
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/ports.py
@@ -0,0 +1,153 @@
+import ipaddress
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+except ImportError:
+ from error import RenderError
+ from validations import (
+ valid_ip_or_raise,
+ valid_port_mode_or_raise,
+ valid_port_or_raise,
+ valid_port_protocol_or_raise,
+ )
+
+
+class Ports:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._ports: dict[str, dict] = {}
+
+ def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str:
+ return f"{host_port}_{host_ip}_{proto}_{ip_family}"
+
+ def _is_wildcard_ip(self, ip: str) -> bool:
+ return ip in ["0.0.0.0", "::"]
+
+ def _get_opposite_wildcard(self, ip: str) -> str:
+ return "0.0.0.0" if ip == "::" else "::"
+
+ def _get_sort_key(self, p: dict) -> str:
+ return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}"
+
+ def _is_ports_same(self, port1: dict, port2: dict) -> bool:
+ return (
+ port1["published"] == port2["published"]
+ and port1["target"] == port2["target"]
+ and port1["protocol"] == port2["protocol"]
+ and port1.get("host_ip", "_") == port2.get("host_ip", "_")
+ )
+
+ def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool:
+ comparison_port = port_config.copy()
+ comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"])
+ for p in wildcard_ports.values():
+ if self._is_ports_same(comparison_port, p):
+ return True
+ return False
+
+ def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None:
+ host_port = port_config["published"]
+ host_ip = port_config["host_ip"]
+ proto = port_config["protocol"]
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip_family)
+
+ if key in self._ports.keys():
+ raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]")
+
+ wildcard_ip = "0.0.0.0" if ip_family == 4 else "::"
+ if host_ip != wildcard_ip:
+ # Check if there is a port with same details but with wildcard IP of the same family
+ wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family)
+ if wildcard_key in self._ports.keys():
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{wildcard_ip}]"
+ )
+ else:
+ # We are adding a port with wildcard IP
+ # Check if there is a port with same details but with specific IP of the same family
+ for p in self._ports.values():
+ # Skip if the port is not for the same family
+ if ip_family != ipaddress.ip_address(p["host_ip"]).version:
+ continue
+
+ # Make a copy of the port config
+ search_port = p.copy()
+ # Replace the host IP with wildcard IP
+ search_port["host_ip"] = wildcard_ip
+ # If the ports match, means that a port for specific IP is already added
+ # and we are trying to add it again with wildcard IP. Raise an error
+ if self._is_ports_same(search_port, port_config):
+ raise RenderError(
+ f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], "
+ f"already bound to [{p['host_ip']}]"
+ )
+
+ def add_port(self, host_port: int, container_port: int, config: dict | None = None):
+ config = config or {}
+ host_port = valid_port_or_raise(host_port)
+ container_port = valid_port_or_raise(container_port)
+ proto = valid_port_protocol_or_raise(config.get("protocol", "tcp"))
+ mode = valid_port_mode_or_raise(config.get("mode", "ingress"))
+
+ # TODO: Once all apps stop using this function directly, (ie using the container.add_port function)
+ # Remove this, and let container.add_port call this for each host_ip
+ host_ip = config.get("host_ip", None)
+ if host_ip is None:
+ self.add_port(host_port, container_port, config | {"host_ip": "0.0.0.0"})
+ self.add_port(host_port, container_port, config | {"host_ip": "::"})
+ return
+
+ host_ip = valid_ip_or_raise(config.get("host_ip", None))
+ ip = ipaddress.ip_address(host_ip)
+
+ port_config = {
+ "published": host_port,
+ "target": container_port,
+ "protocol": proto,
+ "mode": mode,
+ "host_ip": host_ip,
+ }
+ self._check_port_conflicts(port_config, ip.version)
+
+ key = self._gen_port_key(host_port, host_ip, proto, ip.version)
+ self._ports[key] = port_config
+
+ def has_ports(self):
+ return len(self._ports) > 0
+
+ def render(self):
+ specific_ports = []
+ wildcard_ports = {}
+
+ for port_config in self._ports.values():
+ if self._is_wildcard_ip(port_config["host_ip"]):
+ wildcard_ports[id(port_config)] = port_config.copy()
+ else:
+ specific_ports.append(port_config.copy())
+
+ processed_ports = specific_ports.copy()
+ for wild_port in wildcard_ports.values():
+ processed_port = wild_port.copy()
+
+ # Check if there's a matching wildcard port for the opposite IP family
+ has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports)
+
+ if has_opposite_family:
+ processed_port.pop("host_ip")
+
+ if processed_port not in processed_ports:
+ processed_ports.append(processed_port)
+
+ return sorted(processed_ports, key=self._get_sort_key)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/render.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/render.py
new file mode 100644
index 0000000000..9d8fcc28d5
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/render.py
@@ -0,0 +1,89 @@
+import copy
+
+try:
+ from .configs import Configs
+ from .container import Container
+ from .deps import Deps
+ from .error import RenderError
+ from .functions import Functions
+ from .notes import Notes
+ from .portals import Portals
+ from .volumes import Volumes
+except ImportError:
+ from configs import Configs
+ from container import Container
+ from deps import Deps
+ from error import RenderError
+ from functions import Functions
+ from notes import Notes
+ from portals import Portals
+ from volumes import Volumes
+
+
+class Render(object):
+ def __init__(self, values):
+ self._containers: dict[str, Container] = {}
+ self.values = values
+ self._add_images_internal_use()
+ # Make a copy after we inject the images
+ self._original_values: dict = copy.deepcopy(self.values)
+
+ self.deps: Deps = Deps(self)
+
+ self.configs = Configs(render_instance=self)
+ self.funcs = Functions(render_instance=self).func_map()
+ self.portals: Portals = Portals(render_instance=self)
+ self.notes: Notes = Notes(render_instance=self)
+ self.volumes = Volumes(render_instance=self)
+
+ def _add_images_internal_use(self):
+ if not self.values.get("images"):
+ self.values["images"] = {}
+
+ if "python_permissions_image" not in self.values["images"]:
+ self.values["images"]["python_permissions_image"] = {"repository": "python", "tag": "3.13.0-slim-bookworm"}
+
+ def container_names(self):
+ return list(self._containers.keys())
+
+ def add_container(self, name: str, image: str):
+ name = name.strip()
+ if not name:
+ raise RenderError("Container name cannot be empty")
+ container = Container(self, name, image)
+ if name in self._containers:
+ raise RenderError(f"Container {name} already exists.")
+ self._containers[name] = container
+ return container
+
+ def render(self):
+ if self.values != self._original_values:
+ raise RenderError("Values have been modified since the renderer was created.")
+
+ if not self._containers:
+ raise RenderError("No containers added.")
+
+ result: dict = {
+ "x-notes": self.notes.render(),
+ "x-portals": self.portals.render(),
+ "services": {c._name: c.render() for c in self._containers.values()},
+ }
+
+ # Make sure that after services are rendered
+ # there are no labels that target a non-existent container
+ # This is to prevent typos
+ for label in self.values.get("labels", []):
+ for c in label.get("containers", []):
+ if c not in self.container_names():
+ raise RenderError(f"Label [{label['key']}] references container [{c}] which does not exist")
+
+ if self.volumes.has_volumes():
+ result["volumes"] = self.volumes.render()
+
+ if self.configs.has_configs():
+ result["configs"] = self.configs.render()
+
+ # if self.networks:
+ # result["networks"] = {...}
+
+ return result
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/resources.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/resources.py
new file mode 100644
index 0000000000..733f43bb6f
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/resources.py
@@ -0,0 +1,115 @@
+import re
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+except ImportError:
+ from error import RenderError
+
+DEFAULT_CPUS = 2.0
+DEFAULT_MEMORY = 4096
+
+
+class Resources:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._limits: dict = {}
+ self._reservations: dict = {}
+ self._nvidia_ids: set[str] = set()
+ self._auto_add_cpu_from_values()
+ self._auto_add_memory_from_values()
+ self._auto_add_gpus_from_values()
+
+ def _set_cpu(self, cpus: Any):
+ c = str(cpus)
+ if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", c):
+ raise RenderError(f"Expected cpus to be a number or a float (minimum 1.0), got [{cpus}]")
+ self._limits.update({"cpus": c})
+
+ def _set_memory(self, memory: Any):
+ m = str(memory)
+ if not re.match(r"^[1-9][0-9]*$", m):
+ raise RenderError(f"Expected memory to be a number, got [{memory}]")
+ self._limits.update({"memory": f"{m}M"})
+
+ def _auto_add_cpu_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_cpu(resources.get("limits", {}).get("cpus", DEFAULT_CPUS))
+
+ def _auto_add_memory_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ self._set_memory(resources.get("limits", {}).get("memory", DEFAULT_MEMORY))
+
+ def _auto_add_gpus_from_values(self):
+ resources = self._render_instance.values.get("resources", {})
+ gpus = resources.get("gpus", {}).get("nvidia_gpu_selection", {})
+ if not gpus:
+ return
+
+ for pci, gpu in gpus.items():
+ if gpu.get("use_gpu", False):
+ if not gpu.get("uuid"):
+ raise RenderError(f"Expected [uuid] to be set for GPU in slot [{pci}] in [nvidia_gpu_selection]")
+ self._nvidia_ids.add(gpu["uuid"])
+
+ if self._nvidia_ids:
+ if not self._reservations:
+ self._reservations["devices"] = []
+ self._reservations["devices"].append(
+ {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": sorted(self._nvidia_ids),
+ }
+ )
+
+ # This is only used on ix-app that we allow
+ # disabling cpus and memory. GPUs are only added
+ # if the user has requested them.
+ def remove_cpus_and_memory(self):
+ self._limits.pop("cpus", None)
+ self._limits.pop("memory", None)
+
+ # Mainly will be used from dependencies
+ # There is no reason to pass devices to
+ # redis or postgres for example
+ def remove_devices(self):
+ self._reservations.pop("devices", None)
+
+ def set_profile(self, profile: str):
+ cpu, memory = profile_mapping(profile)
+ self._set_cpu(cpu)
+ self._set_memory(memory)
+
+ def has_resources(self):
+ return len(self._limits) > 0 or len(self._reservations) > 0
+
+ def has_gpus(self):
+ gpu_devices = [d for d in self._reservations.get("devices", []) if "gpu" in d["capabilities"]]
+ return len(gpu_devices) > 0
+
+ def render(self):
+ result = {}
+ if self._limits:
+ result["limits"] = self._limits
+ if self._reservations:
+ result["reservations"] = self._reservations
+
+ return result
+
+
+def profile_mapping(profile: str):
+ profiles = {
+ "low": (1, 512),
+ "medium": (2, 1024),
+ }
+
+ if profile not in profiles:
+ raise RenderError(
+ f"Resource profile [{profile}] is not valid. Valid options are: [{', '.join(profiles.keys())}]"
+ )
+
+ return profiles[profile]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/restart.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/restart.py
new file mode 100644
index 0000000000..2f6281af48
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/restart.py
@@ -0,0 +1,25 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .validations import valid_restart_policy_or_raise
+except ImportError:
+ from validations import valid_restart_policy_or_raise
+
+
+class RestartPolicy:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._policy: str = "unless-stopped"
+ self._maximum_retry_count: int = 0
+
+ def set_policy(self, policy: str, maximum_retry_count: int = 0):
+ self._policy = valid_restart_policy_or_raise(policy, maximum_retry_count)
+ self._maximum_retry_count = maximum_retry_count
+
+ def render(self):
+ if self._policy == "on-failure" and self._maximum_retry_count > 0:
+ return f"{self._policy}:{self._maximum_retry_count}"
+ return self._policy
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/storage.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/storage.py
new file mode 100644
index 0000000000..f1650259b3
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/storage.py
@@ -0,0 +1,116 @@
+from typing import TYPE_CHECKING, TypedDict, Literal, NotRequired, Union
+
+if TYPE_CHECKING:
+ from render import Render
+
+try:
+ from .error import RenderError
+ from .validations import valid_fs_path_or_raise
+ from .volume_mount import VolumeMount
+except ImportError:
+ from error import RenderError
+ from validations import valid_fs_path_or_raise
+ from volume_mount import VolumeMount
+
+
+class IxStorageTmpfsConfig(TypedDict):
+ size: NotRequired[int]
+ mode: NotRequired[str]
+
+
+class AclConfig(TypedDict, total=False):
+ path: str
+
+
+class IxStorageHostPathConfig(TypedDict):
+ path: NotRequired[str] # Either this or acl.path must be set
+ acl_enable: NotRequired[bool]
+ acl: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageIxVolumeConfig(TypedDict):
+ dataset_name: str
+ acl_enable: NotRequired[bool]
+ acl_entries: NotRequired[AclConfig]
+ create_host_path: NotRequired[bool]
+ propagation: NotRequired[Literal["shared", "slave", "private", "rshared", "rslave", "rprivate"]]
+ auto_permissions: NotRequired[bool] # Only when acl_enable is false
+
+
+class IxStorageVolumeConfig(TypedDict):
+ volume_name: NotRequired[str]
+ nocopy: NotRequired[bool]
+ auto_permissions: NotRequired[bool]
+
+
+class IxStorageNfsConfig(TypedDict):
+ server: str
+ path: str
+ options: NotRequired[list[str]]
+
+
+class IxStorageCifsConfig(TypedDict):
+ server: str
+ path: str
+ username: str
+ password: str
+ domain: NotRequired[str]
+ options: NotRequired[list[str]]
+
+
+IxStorageVolumeLikeConfigs = Union[IxStorageVolumeConfig, IxStorageNfsConfig, IxStorageCifsConfig, IxStorageTmpfsConfig]
+IxStorageBindLikeConfigs = Union[IxStorageHostPathConfig, IxStorageIxVolumeConfig]
+IxStorageLikeConfigs = Union[IxStorageBindLikeConfigs, IxStorageVolumeLikeConfigs]
+
+
+class IxStorage(TypedDict):
+ type: Literal["ix_volume", "host_path", "tmpfs", "volume", "anonymous", "temporary"]
+ read_only: NotRequired[bool]
+
+ ix_volume_config: NotRequired[IxStorageIxVolumeConfig]
+ host_path_config: NotRequired[IxStorageHostPathConfig]
+ tmpfs_config: NotRequired[IxStorageTmpfsConfig]
+ volume_config: NotRequired[IxStorageVolumeConfig]
+ nfs_config: NotRequired[IxStorageNfsConfig]
+ cifs_config: NotRequired[IxStorageCifsConfig]
+
+
+class Storage:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volume_mounts: set[VolumeMount] = set()
+
+ def add(self, mount_path: str, config: "IxStorage"):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ if mount_path in [m.mount_path for m in self._volume_mounts]:
+ raise RenderError(f"Mount path [{mount_path}] already used for another volume mount")
+
+ volume_mount = VolumeMount(self._render_instance, mount_path, config)
+ self._volume_mounts.add(volume_mount)
+
+ def _add_docker_socket(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/var/run/docker.sock", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def _add_udev(self, read_only: bool = True, mount_path: str = ""):
+ mount_path = valid_fs_path_or_raise(mount_path)
+ cfg: "IxStorage" = {
+ "type": "host_path",
+ "read_only": read_only,
+ "host_path_config": {"path": "/run/udev", "create_host_path": False},
+ }
+ self.add(mount_path, cfg)
+
+ def has_mounts(self) -> bool:
+ return bool(self._volume_mounts)
+
+ def render(self):
+ return [vm.render() for vm in sorted(self._volume_mounts, key=lambda vm: vm.mount_path)]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/sysctls.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/sysctls.py
new file mode 100644
index 0000000000..e6b8469f3b
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/sysctls.py
@@ -0,0 +1,38 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from container import Container
+
+try:
+ from .error import RenderError
+ from .validations import valid_sysctl_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_sysctl_or_raise
+
+
+class Sysctls:
+ def __init__(self, render_instance: "Render", container_instance: "Container"):
+ self._render_instance = render_instance
+ self._container_instance = container_instance
+ self._sysctls: dict = {}
+
+ def add(self, key: str, value):
+ key = key.strip()
+ if not key:
+ raise RenderError("Sysctls key cannot be empty")
+ if value is None:
+ raise RenderError(f"Sysctl [{key}] requires a value")
+ if key in self._sysctls:
+ raise RenderError(f"Sysctl [{key}] already added")
+ self._sysctls[key] = str(value)
+
+ def has_sysctls(self):
+ return bool(self._sysctls)
+
+ def render(self):
+ if not self.has_sysctls():
+ return {}
+ host_net = self._container_instance._network_mode == "host"
+ return {valid_sysctl_or_raise(k, host_net): v for k, v in self._sysctls.items()}
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/__init__.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_build_image.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_build_image.py
new file mode 100644
index 0000000000..f30c1210ed
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_build_image.py
@@ -0,0 +1,49 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_build_image_with_from(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.build_image(["FROM test_image"])
+
+
+def test_build_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.build_image(
+ [
+ "RUN echo hello",
+ None,
+ "",
+ "RUN echo world",
+ ]
+ )
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["image"]
+ == "ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"
+ )
+ assert output["services"]["test_container"]["build"] == {
+ "tags": ["ix-nginx:latest_4a127145ea4c25511707e57005dd0ed457fe2f4932082c8f9faa339a450b6a99"],
+ "dockerfile_inline": """FROM nginx:latest
+RUN echo hello
+RUN echo world
+""",
+ }
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_configs.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_configs.py
new file mode 100644
index 0000000000..9049e473ea
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_configs.py
@@ -0,0 +1,63 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_duplicate_config_with_different_data(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data2", "/some/path")
+
+
+def test_add_config_with_empty_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.configs.add("test_config", "test_data", "")
+
+
+def test_add_duplicate_target(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path")
+ with pytest.raises(Exception):
+ c1.configs.add("test_config2", "test_data2", "/some/path")
+
+
+def test_add_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "$test_data", "/some/path")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "$$test_data"
+ assert output["services"]["test_container"]["configs"] == [{"source": "test_config", "target": "/some/path"}]
+
+
+def test_add_config_with_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.configs.add("test_config", "test_data", "/some/path", "0777")
+ output = render.render()
+ assert output["configs"]["test_config"]["content"] == "test_data"
+ assert output["services"]["test_container"]["configs"] == [
+ {"source": "test_config", "target": "/some/path", "mode": 511}
+ ]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_container.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_container.py
new file mode 100644
index 0000000000..1980dcd5df
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_container.py
@@ -0,0 +1,425 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_empty_container_name(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container(" ", "test_image")
+
+
+def test_resolve_image(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["image"] == "nginx:latest"
+
+
+def test_missing_repo(mock_values):
+ mock_values["images"]["test_image"]["repository"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_missing_tag(mock_values):
+ mock_values["images"]["test_image"]["tag"] = ""
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_non_existing_image(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "non_existing_image")
+
+
+def test_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_pull_policy("always")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["pull_policy"] == "always"
+
+
+def test_invalid_pull_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.set_pull_policy("invalid_policy")
+
+
+def test_clear_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.add_caps(["NET_ADMIN"])
+ c1.clear_caps()
+ c1.healthcheck.disable()
+ output = render.render()
+ assert "cap_drop" not in output["services"]["test_container"]
+ assert "cap_add" not in output["services"]["test_container"]
+
+
+def test_privileged(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_privileged(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["privileged"] is True
+
+
+def test_tty(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_tty(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["tty"] is True
+
+
+def test_init(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_init(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["init"] is True
+
+
+def test_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_read_only(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["read_only"] is True
+
+
+def test_stdin(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_stdin(True)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stdin_open"] is True
+
+
+def test_hostname(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_hostname("test_hostname")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["hostname"] == "test_hostname"
+
+
+def test_grace_period(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_grace_period(10)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["stop_grace_period"] == "10s"
+
+
+def test_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_user(1000, 1000)
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["user"] == "1000:1000"
+
+
+def test_invalid_user(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_user(-100, 1000)
+
+
+def test_add_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ c1.add_group("video")
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
+
+
+def test_add_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_group(1000)
+ with pytest.raises(Exception):
+ c1.add_group(1000)
+
+
+def test_add_group_as_string(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_group("1000")
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_docker_socket()
+ output = render.render()
+ assert output["services"]["test_container"]["group_add"] == [568, 999]
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {
+ "propagation": "rprivate",
+ "create_host_path": False,
+ },
+ }
+ ]
+
+
+def test_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_shm_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_shm_size_mb(10)
+ output = render.render()
+ assert output["services"]["test_container"]["shm_size"] == "10M"
+
+
+def test_valid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_caps(["ALL", "NET_ADMIN"])
+ output = render.render()
+ assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
+ assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
+
+
+def test_add_duplicate_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
+
+
+def test_invalid_caps(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_caps(["invalid_cap"])
+
+
+def test_remove_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.remove_security_opt("no-new-privileges")
+ output = render.render()
+ assert "security_opt" not in output["services"]["test_container"]
+
+
+def test_add_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_security_opt("seccomp=unconfined")
+ output = render.render()
+ assert output["services"]["test_container"]["security_opt"] == [
+ "no-new-privileges",
+ "seccomp=unconfined",
+ ]
+
+
+def test_add_duplicate_security_opt(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_security_opt("no-new-privileges")
+
+
+def test_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_auto_network_mode_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "host"
+
+
+def test_network_mode_with_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("service:test_container")
+ output = render.render()
+ assert output["services"]["test_container"]["network_mode"] == "service:test_container"
+
+
+def test_network_mode_with_container_missing(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("service:missing_container")
+
+
+def test_invalid_network_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_network_mode("invalid_mode")
+
+
+def test_entrypoint(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
+
+
+def test_command(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.set_command(["echo", "hello $MY_ENV"])
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
+
+
+def test_add_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
+ c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
+ c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
+ c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
+ c1.add_port(
+ {"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
+ {"container_port": 9092, "protocol": "udp"},
+ )
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
+ ]
+ assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
+
+
+def test_add_ports_with_invalid_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
+
+
+def test_add_ports_with_empty_host_ips(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
+ ]
+
+
+def test_set_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("host")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "host"
+
+
+def test_set_ipc_empty_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_ipc_mode("")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == ""
+
+
+def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("invalid")
+
+
+def test_set_ipc_mode_with_container_ipc_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c2 = render.add_container("test_container2", "test_image")
+ c2.healthcheck.disable()
+ c1.set_ipc_mode("container:test_container2")
+ output = render.render()
+ assert output["services"]["test_container"]["ipc"] == "container:test_container2"
+
+
+def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.set_ipc_mode("container:invalid")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_depends.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_depends.py
new file mode 100644
index 0000000000..a1d8373927
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_depends.py
@@ -0,0 +1,54 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_dependency(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ c1.depends.add_dependency("test_container2", "service_started")
+ output = render.render()
+ assert output["services"]["test_container"]["depends_on"]["test_container2"] == {"condition": "service_started"}
+
+
+def test_add_dependency_invalid_condition(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ render.add_container("test_container2", "test_image")
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "invalid_condition")
+
+
+def test_add_dependency_missing_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
+
+
+def test_add_dependency_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ render.add_container("test_container2", "test_image")
+ c1.depends.add_dependency("test_container2", "service_started")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.depends.add_dependency("test_container2", "service_started")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_deps.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_deps.py
new file mode 100644
index 0000000000..a1b7f03a60
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_deps.py
@@ -0,0 +1,477 @@
+import json
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_postgres_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "16"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ p = render.deps.postgres(
+ "pg_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_@password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ p.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert (
+ p.get_url("postgres") == "postgres://test_user:test_%40password@pg_container:5432/test_database?sslmode=disable"
+ )
+ assert "devices" not in output["services"]["pg_container"]
+ assert "reservations" not in output["services"]["pg_container"]["deploy"]["resources"]
+ assert output["services"]["pg_container"]["image"] == "postgres:16"
+ assert output["services"]["pg_container"]["user"] == "999:999"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["pg_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["pg_container"]["healthcheck"] == {
+ "test": "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["pg_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/postgresql/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["pg_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "POSTGRES_USER": "test_user",
+ "POSTGRES_PASSWORD": "test_@password",
+ "POSTGRES_DB": "test_database",
+ "POSTGRES_PORT": "5432",
+ }
+ assert output["services"]["pg_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"},
+ "pg_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert output["services"]["perms_container"]["restart"] == "on-failure:1"
+
+
+def test_add_redis_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test_password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis_with_password_with_spaces(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.redis(
+ "test_container",
+ "test_image",
+ {"password": "test password", "volume": {}}, # type: ignore
+ )
+
+
+def test_add_redis(mock_values):
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ r = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test&password@",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ c1.environment.add_env("REDIS_URL", r.get_url("redis"))
+ if perms_container.has_actions():
+ perms_container.activate()
+ r.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["redis_container"]
+ assert "reservations" not in output["services"]["redis_container"]["deploy"]["resources"]
+ assert (
+ output["services"]["test_container"]["environment"]["REDIS_URL"]
+ == "redis://default:test%26password%40@redis_container:6379"
+ )
+ assert output["services"]["redis_container"]["image"] == "redis:latest"
+ assert output["services"]["redis_container"]["user"] == "1001:0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["redis_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["redis_container"]["healthcheck"] == {
+ "test": "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["redis_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/bitnami/redis/data",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["redis_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "ALLOW_EMPTY_PASSWORD": "no",
+ "REDIS_PASSWORD": "test&password@",
+ "REDIS_PORT_NUMBER": "6379",
+ }
+ assert output["services"]["redis_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_mariadb_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.mariadb(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_mariadb(mock_values):
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("perms_container")
+ m = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ m.container.depends.add_dependency("perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "devices" not in output["services"]["mariadb_container"]
+ assert "reservations" not in output["services"]["mariadb_container"]["deploy"]["resources"]
+ assert output["services"]["mariadb_container"]["image"] == "mariadb:latest"
+ assert output["services"]["mariadb_container"]["user"] == "999:999"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["cpus"] == "2.0"
+ assert output["services"]["mariadb_container"]["deploy"]["resources"]["limits"]["memory"] == "4096M"
+ assert output["services"]["mariadb_container"]["healthcheck"] == {
+ "test": "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+ assert output["services"]["mariadb_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/var/lib/mysql",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["services"]["mariadb_container"]["environment"] == {
+ "TZ": "Etc/UTC",
+ "UMASK": "002",
+ "UMASK_SET": "002",
+ "NVIDIA_VISIBLE_DEVICES": "void",
+ "MARIADB_USER": "test_user",
+ "MARIADB_PASSWORD": "test_password",
+ "MARIADB_ROOT_PASSWORD": "test_password",
+ "MARIADB_DATABASE": "test_database",
+ "MARIADB_AUTO_UPGRADE": "true",
+ }
+ assert output["services"]["mariadb_container"]["depends_on"] == {
+ "perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_add_perms_container(mock_values):
+ mock_values["ix_volumes"] = {
+ "test_dataset1": "/mnt/test/1",
+ "test_dataset2": "/mnt/test/2",
+ "test_dataset3": "/mnt/test/3",
+ }
+ mock_values["images"]["postgres_image"] = {"repository": "postgres", "tag": "17"}
+ mock_values["images"]["redis_image"] = {"repository": "redis", "tag": "latest"}
+ mock_values["images"]["mariadb_image"] = {"repository": "mariadb", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ # fmt: off
+ volume_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ volume_no_perms = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ host_path_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "auto_permissions": True}}
+ host_path_no_perms = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ host_path_acl_perms = {"type": "host_path", "host_path_config": {"acl": {"path": "/mnt/test"}, "acl_enable": True, "auto_permissions": True}} # noqa
+ ix_volume_no_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset1"}}
+ ix_volume_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset2", "auto_permissions": True}} # noqa
+ ix_volume_acl_perms = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset3", "acl_enable": True, "auto_permissions": True}} # noqa
+ temp_volume = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ # fmt: on
+
+ c1.add_storage("/some/path1", volume_perms)
+ c1.add_storage("/some/path2", volume_no_perms)
+ c1.add_storage("/some/path3", host_path_perms)
+ c1.add_storage("/some/path4", host_path_no_perms)
+ c1.add_storage("/some/path5", host_path_acl_perms)
+ c1.add_storage("/some/path6", ix_volume_no_perms)
+ c1.add_storage("/some/path7", ix_volume_perms)
+ c1.add_storage("/some/path8", ix_volume_acl_perms)
+ c1.add_storage("/some/path9", temp_volume)
+
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data2", volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data3", host_path_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data4", host_path_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data5", host_path_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data6", ix_volume_no_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data7", ix_volume_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data8", ix_volume_acl_perms, {"uid": 1000, "gid": 1000, "mode": "check"})
+ perms_container.add_or_skip_action("data9", temp_volume, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+ postgres = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ redis = render.deps.redis(
+ "redis_container",
+ "redis_image",
+ {
+ "password": "test_password",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ mariadb = render.deps.mariadb(
+ "mariadb_container",
+ "mariadb_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ postgres.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ redis.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ mariadb.container.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert output["services"]["test_perms_container"]["network_mode"] == "none"
+ assert output["services"]["test_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+ assert output["configs"]["permissions_run_script"]["content"] != ""
+ # fmt: off
+ content = [
+ {"mount_path": "/mnt/permission/data", "is_temporary": False, "identifier": "data", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data3", "is_temporary": False, "identifier": "data3", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data6", "is_temporary": False, "identifier": "data6", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data7", "is_temporary": False, "identifier": "data7", "recursive": False, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/data9", "is_temporary": True, "identifier": "data9", "recursive": True, "mode": "check", "uid": 1000, "gid": 1000, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/postgres_container_postgres_data", "is_temporary": False, "identifier": "postgres_container_postgres_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/redis_container_redis_data", "is_temporary": False, "identifier": "redis_container_redis_data", "recursive": False, "mode": "check", "uid": 1001, "gid": 0, "chmod": None}, # noqa
+ {"mount_path": "/mnt/permission/mariadb_container_mariadb_data", "is_temporary": False, "identifier": "mariadb_container_mariadb_data", "recursive": False, "mode": "check", "uid": 999, "gid": 999, "chmod": None}, # noqa
+ ]
+ # fmt: on
+ assert output["configs"]["permissions_actions_data"]["content"] == json.dumps(content)
+
+
+def test_add_duplicate_perms_action(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ with pytest.raises(Exception):
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+
+
+def test_add_perm_action_without_auto_perms_enabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": False}}
+ c1.add_storage("/some/path", vol_config)
+ perms_container = render.deps.perms("test_perms_container")
+ perms_container.add_or_skip_action("data", vol_config, {"uid": 1000, "gid": 1000, "mode": "check"})
+ if perms_container.has_actions():
+ perms_container.activate()
+ c1.depends.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert "configs" not in output
+ assert "ix-test_perms_container" not in output["services"]
+ assert "depends_on" not in output["services"]["test_container"]
+
+
+def test_add_unsupported_postgres_version(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "99"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_add_postgres_with_invalid_tag(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": "latest"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.deps.postgres(
+ "test_container",
+ "test_image",
+ {"user": "test_user", "password": "test_password", "database": "test_database"}, # type: ignore
+ )
+
+
+def test_no_upgrade_container_with_non_postgres_image(mock_values):
+ mock_values["images"]["postgres_image"] = {"repository": "tensorchord/pgvecto-rs", "tag": "pg15-v0.2.0"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "postgres_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ assert len(output["services"]) == 3 # c1, pg, perms
+ assert output["services"]["postgres_container"]["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"}
+ }
+
+
+def test_postgres_with_upgrade_container(mock_values):
+ mock_values["images"]["pg_image"] = {"repository": "postgres", "tag": 16.6}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ perms_container = render.deps.perms("test_perms_container")
+ pg = render.deps.postgres(
+ "postgres_container",
+ "pg_image",
+ {
+ "user": "test_user",
+ "password": "test_password",
+ "database": "test_database",
+ "volume": {"type": "volume", "volume_config": {"volume_name": "test_volume", "auto_permissions": True}},
+ },
+ perms_container,
+ )
+ if perms_container.has_actions():
+ perms_container.activate()
+ pg.add_dependency("test_perms_container", "service_completed_successfully")
+ output = render.render()
+ pg = output["services"]["postgres_container"]
+ pgup = output["services"]["postgres_container_upgrade"]
+ assert pg["volumes"] == pgup["volumes"]
+ assert pg["user"] == pgup["user"]
+ assert pgup["environment"]["TARGET_VERSION"] == "16"
+ assert pgup["environment"]["DATA_DIR"] == "/var/lib/postgresql/data"
+ pgup_env = pgup["environment"]
+ pgup_env.pop("TARGET_VERSION")
+ pgup_env.pop("DATA_DIR")
+ assert pg["environment"] == pgup_env
+ assert pg["depends_on"] == {
+ "test_perms_container": {"condition": "service_completed_successfully"},
+ "postgres_container_upgrade": {"condition": "service_completed_successfully"},
+ }
+ assert pgup["depends_on"] == {"test_perms_container": {"condition": "service_completed_successfully"}}
+ assert pgup["restart"] == "on-failure:1"
+ assert pgup["healthcheck"] == {"disable": True}
+ assert pgup["build"]["dockerfile_inline"] != ""
+ assert pgup["configs"][0]["source"] == "pg_container_upgrade.sh"
+ assert pgup["entrypoint"] == ["/bin/bash", "-c", "/upgrade.sh"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device.py
new file mode 100644
index 0000000000..2e71daa5a0
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device.py
@@ -0,0 +1,150 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ c1.devices.add_device("/h/dev/sdb", "/c/dev/sdb", "rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/h/dev/sda:/c/dev/sda", "/h/dev/sdb:/c/dev/sdb:rwm"]
+
+
+def test_devices_without_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("", "/c/dev/sda")
+
+
+def test_devices_without_container(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "")
+
+
+def test_add_duplicate_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_container_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "c/dev/sda")
+
+
+def test_add_device_with_invalid_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("h/dev/sda", "/c/dev/sda")
+
+
+def test_add_disallowed_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/dri", "/c/dev/sda")
+
+
+def test_add_device_with_invalid_cgroup_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/h/dev/sda", "/c/dev/sda", "invalid")
+
+
+def test_automatically_add_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_automatically_add_gpu_devices_and_kfd(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True, "kfd_device_exists": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/dri:/dev/dri", "/dev/kfd:/dev/kfd"]
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_remove_gpu_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"use_all_gpus": True}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.remove_devices()
+ output = render.render()
+ assert "devices" not in output["services"]["test_container"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_add_usb_bus(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.devices.add_usb_bus()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/bus/usb:/dev/bus/usb"]
+
+
+def test_add_usb_bus_disallowed(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.devices.add_device("/dev/bus/usb", "/dev/bus/usb")
+
+
+def test_add_snd_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_snd_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
+ assert output["services"]["test_container"]["group_add"] == [29, 568]
+
+
+def test_add_tun_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_tun_device()
+ output = render.render()
+ assert output["services"]["test_container"]["devices"] == ["/dev/net/tun:/dev/net/tun"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
new file mode 100644
index 0000000000..581fe82017
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_device_cgroup_rules.py
@@ -0,0 +1,79 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_device_cgroup_rule(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ c1.add_device_cgroup_rule("b 10:20 rwm")
+ output = render.render()
+ assert output["services"]["test_container"]["device_cgroup_rules"] == [
+ "b 10:20 rwm",
+ "c 13:* rwm",
+ ]
+
+
+def test_device_cgroup_rule_duplicate(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rwm")
+
+
+def test_device_cgroup_rule_duplicate_group(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_device_cgroup_rule("c 13:* rwm")
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("c 13:* rm")
+
+
+def test_device_cgroup_rule_invalid_device(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("d 10:20 rwm")
+
+
+def test_device_cgroup_rule_invalid_perm(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10:20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 20 rwd")
+
+
+def test_device_cgroup_rule_invalid_format_missing_major(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_device_cgroup_rule("a 10 rwd")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_dns.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_dns.py
new file mode 100644
index 0000000000..fe6b21e34f
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_dns.py
@@ -0,0 +1,64 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "opt1", "opt2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_opt"] == ["attempts:3", "opt1", "opt2"]
+
+
+def test_auto_add_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns_search"] == ["search1", "search2"]
+
+
+def test_auto_add_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver2"]}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["dns"] == ["nameserver1", "nameserver2"]
+
+
+def test_add_duplicate_dns_nameservers(mock_values):
+ mock_values["network"] = {"dns_nameservers": ["nameserver1", "nameserver1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_searches(mock_values):
+ mock_values["network"] = {"dns_searches": ["search1", "search1"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_add_duplicate_dns_opts(mock_values):
+ mock_values["network"] = {"dns_opts": ["attempts:3", "attempts:5"]}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_environment.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_environment.py
new file mode 100644
index 0000000000..d657646582
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_environment.py
@@ -0,0 +1,196 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_auto_add_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ mock_values["run_as"] = {"user": "1000", "group": "1000"}
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert len(envs) == 11
+ assert envs["TZ"] == "Etc/UTC"
+ assert envs["PUID"] == "1000"
+ assert envs["UID"] == "1000"
+ assert envs["USER_ID"] == "1000"
+ assert envs["PGID"] == "1000"
+ assert envs["GID"] == "1000"
+ assert envs["GROUP_ID"] == "1000"
+ assert envs["UMASK"] == "002"
+ assert envs["UMASK_SET"] == "002"
+ assert envs["NVIDIA_DRIVER_CAPABILITIES"] == "all"
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "uuid_0,uuid_1"
+
+
+def test_skip_generic_variables(mock_values):
+ mock_values["skip_generic_variables"] = True
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+
+ assert len(envs) == 1
+ assert envs["NVIDIA_VISIBLE_DEVICES"] == "void"
+
+
+def test_add_from_all_sources(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_value")
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_value"
+ assert envs["USER_ENV"] == "test_value2"
+ assert envs["TZ"] == "Etc/UTC"
+
+
+def test_user_add_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV2", "value": "test_value2"},
+ ]
+ )
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["MY_ENV"] == "test_value"
+ assert envs["MY_ENV2"] == "test_value2"
+
+
+def test_user_add_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "MY_ENV", "value": "test_value"},
+ {"name": "MY_ENV", "value": "test_value2"},
+ ]
+ )
+
+
+def test_user_env_without_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_user_envs(
+ [
+ {"name": "", "value": "test_value"},
+ ]
+ )
+
+
+def test_user_env_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "TZ", "value": "test_value"},
+ ]
+ )
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_user_env_try_to_overwrite_app_dev_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_user_envs(
+ [
+ {"name": "PORT", "value": "test_value"},
+ ]
+ )
+ c1.environment.add_env("PORT", "test_value2")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_vars_try_to_overwrite_auto_vars(mock_values):
+ mock_values["TZ"] = "Etc/UTC"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("TZ", "test_value")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_app_dev_no_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.environment.add_env("", "test_value")
+
+
+def test_app_dev_duplicate_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("PORT", "test_value")
+ with pytest.raises(Exception):
+ c1.environment.add_env("PORT", "test_value2")
+
+
+def test_format_vars(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.environment.add_env("APP_ENV", "test_$value")
+ c1.environment.add_env("APP_ENV_BOOL", True)
+ c1.environment.add_env("APP_ENV_INT", 10)
+ c1.environment.add_env("APP_ENV_FLOAT", 10.5)
+ c1.environment.add_user_envs(
+ [
+ {"name": "USER_ENV", "value": "test_$value2"},
+ ]
+ )
+
+ output = render.render()
+ envs = output["services"]["test_container"]["environment"]
+ assert envs["APP_ENV"] == "test_$$value"
+ assert envs["USER_ENV"] == "test_$$value2"
+ assert envs["APP_ENV_BOOL"] == "true"
+ assert envs["APP_ENV_INT"] == "10"
+ assert envs["APP_ENV_FLOAT"] == "10.5"
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_expose.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_expose.py
new file mode 100644
index 0000000000..b8724d7548
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_expose.py
@@ -0,0 +1,46 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ c1.expose.add_port(8081, "udp")
+ c1.expose.add_port(8082, "udp")
+ output = render.render()
+ assert output["services"]["test_container"]["expose"] == ["8081/tcp", "8081/udp", "8082/udp"]
+
+
+def test_add_duplicate_expose_ports(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ with pytest.raises(Exception):
+ c1.expose.add_port(8081)
+
+
+def test_add_expose_ports_with_host_network(mock_values):
+ mock_values["network"] = {"host_network": True}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.expose.add_port(8081)
+ output = render.render()
+ assert "expose" not in output["services"]["test_container"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
new file mode 100644
index 0000000000..35230be16e
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_extra_hosts.py
@@ -0,0 +1,57 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ c1.add_extra_host("test_host2", "127.0.0.2")
+ c1.add_extra_host("host.docker.internal", "host-gateway")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {
+ "host.docker.internal": "host-gateway",
+ "test_host": "127.0.0.1",
+ "test_host2": "127.0.0.2",
+ }
+
+
+def test_add_duplicate_extra_host(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "127.0.0.1")
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "127.0.0.2")
+
+
+def test_add_extra_host_with_ipv6(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_extra_host("test_host", "::1")
+ output = render.render()
+ assert output["services"]["test_container"]["extra_hosts"] == {"test_host": "::1"}
+
+
+def test_add_extra_host_with_invalid_ip(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_extra_host("test_host", "invalid_ip")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_formatter.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_formatter.py
new file mode 100644
index 0000000000..843cf65d2e
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_formatter.py
@@ -0,0 +1,13 @@
+from formatter import escape_dollar
+
+
+def test_escape_dollar():
+ cases = [
+ {"input": "test", "expected": "test"},
+ {"input": "$test", "expected": "$$test"},
+ {"input": "$$test", "expected": "$$$$test"},
+ {"input": "$$$test", "expected": "$$$$$$test"},
+ {"input": "$test$", "expected": "$$test$$"},
+ ]
+ for case in cases:
+ assert escape_dollar(case["input"]) == case["expected"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_functions.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_functions.py
new file mode 100644
index 0000000000..0ea3b57d18
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_functions.py
@@ -0,0 +1,88 @@
+import re
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_funcs(mock_values):
+ mock_values["ix_volumes"] = {"test": "/mnt/test123"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ tests = [
+ {"func": "auto_cast", "values": ["1"], "expected": 1},
+ {"func": "basic_auth_header", "values": ["my_user", "my_pass"], "expected": "Basic bXlfdXNlcjpteV9wYXNz"},
+ {"func": "basic_auth", "values": ["my_user", "my_pass"], "expected": "bXlfdXNlcjpteV9wYXNz"},
+ {
+ "func": "bcrypt_hash",
+ "values": ["my_pass"],
+ "expect_regex": r"^\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "camel_case", "values": ["my_user"], "expected": "My_User"},
+ {"func": "copy_dict", "values": [{"a": 1}], "expected": {"a": 1}},
+ {"func": "fail", "values": ["my_message"], "expect_raise": True},
+ {
+ "func": "htpasswd",
+ "values": ["my_user", "my_pass"],
+ "expect_regex": r"^my_user:\$2b\$12\$[a-zA-Z0-9-_\.\/]+$",
+ },
+ {"func": "is_boolean", "values": ["true"], "expected": True},
+ {"func": "is_boolean", "values": ["false"], "expected": True},
+ {"func": "is_number", "values": ["1"], "expected": True},
+ {"func": "is_number", "values": ["1.1"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[a-zA-Z0-9]+$"], "expected": True},
+ {"func": "match_regex", "values": ["value", "^[0-9]+$"], "expected": False},
+ {"func": "merge_dicts", "values": [{"a": 1}, {"b": 2}], "expected": {"a": 1, "b": 2}},
+ {"func": "must_match_regex", "values": ["my_user", "^[0-9]$"], "expect_raise": True},
+ {"func": "must_match_regex", "values": ["1", "^[0-9]$"], "expected": "1"},
+ {"func": "secure_string", "values": [10], "expect_regex": r"^[a-zA-Z0-9-_]+$"},
+ {"func": "disallow_chars", "values": ["my_user", ["$", "@"], "my_key"], "expected": "my_user"},
+ {"func": "disallow_chars", "values": ["my_user$", ["$", "@"], "my_key"], "expect_raise": True},
+ {
+ "func": "get_host_path",
+ "values": [{"type": "host_path", "host_path_config": {"path": "/mnt/test"}}],
+ "expected": "/mnt/test",
+ },
+ {
+ "func": "get_host_path",
+ "values": [{"type": "ix_volume", "ix_volume_config": {"dataset_name": "test"}}],
+ "expected": "/mnt/test123",
+ },
+ {"func": "or_default", "values": [None, 1], "expected": 1},
+ {"func": "or_default", "values": [1, None], "expected": 1},
+ {"func": "or_default", "values": [False, 1], "expected": 1},
+ {"func": "or_default", "values": [True, 1], "expected": True},
+ {"func": "temp_config", "values": [""], "expect_raise": True},
+ {
+ "func": "temp_config",
+ "values": ["test"],
+ "expected": {"type": "temporary", "volume_config": {"volume_name": "test"}},
+ },
+ ]
+
+ for test in tests:
+ print(test["func"], test)
+ func = render.funcs[test["func"]]
+ if test.get("expect_raise", False):
+ with pytest.raises(Exception):
+ func(*test["values"])
+ elif test.get("expect_regex"):
+ r = func(*test["values"])
+ assert re.match(test["expect_regex"], r) is not None
+ else:
+ r = func(*test["values"])
+ assert r == test["expected"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_healthcheck.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
new file mode 100644
index 0000000000..8fa044290f
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_healthcheck.py
@@ -0,0 +1,195 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_disable_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {"disable": True}
+
+
+def test_use_built_in_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.use_built_in()
+ output = render.render()
+ assert "healthcheck" not in output["services"]["test_container"]
+
+
+def test_set_custom_test(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test("echo $1")
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": "echo $$1",
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_custom_test_array(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "10s",
+ "timeout": "5s",
+ "retries": 30,
+ "start_period": "10s",
+ }
+
+
+def test_set_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_custom_test(["CMD", "echo", "$1"])
+ c1.healthcheck.set_interval(9)
+ c1.healthcheck.set_timeout(8)
+ c1.healthcheck.set_retries(7)
+ c1.healthcheck.set_start_period(6)
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"] == {
+ "test": ["CMD", "echo", "$$1"],
+ "interval": "9s",
+ "timeout": "8s",
+ "retries": 7,
+ "start_period": "6s",
+ }
+
+
+def test_adding_test_when_disabled(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.healthcheck.set_custom_test("echo $1")
+
+
+def test_not_adding_test(mock_values):
+ render = Render(mock_values)
+ render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_invalid_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ with pytest.raises(Exception):
+ c1.healthcheck.set_test("http", {"port": 8080, "path": "invalid"})
+
+
+def test_http_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("http", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == """/bin/bash -c 'exec {hc_fd}<>/dev/tcp/127.0.0.1/8080 && echo -e "GET / HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n" >&$${hc_fd} && cat <&$${hc_fd} | grep "HTTP" | grep -q "200"'""" # noqa
+ )
+
+
+def test_curl_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "curl --silent --output /dev/null --show-error --fail http://127.0.0.1:8080/health"
+ )
+
+
+def test_curl_healthcheck_with_headers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("curl", {"port": 8080, "path": "/health", "headers": [("X-Test", "$test")]})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == 'curl --silent --output /dev/null --show-error --fail --header "X-Test: $$test" http://127.0.0.1:8080/health'
+ )
+
+
+def test_wget_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("wget", {"port": 8080, "path": "/health"})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "wget --spider --quiet http://127.0.0.1:8080/health"
+ )
+
+
+def test_netcat_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("netcat", {"port": 8080})
+ output = render.render()
+ assert output["services"]["test_container"]["healthcheck"]["test"] == "nc -z -w 1 127.0.0.1 8080"
+
+
+def test_tcp_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("tcp", {"port": 8080})
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/8080'"
+ )
+
+
+def test_redis_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("redis")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "redis-cli -h 127.0.0.1 -p 6379 -a $$REDIS_PASSWORD ping | grep -q PONG"
+ )
+
+
+def test_postgres_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("postgres")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB"
+ )
+
+
+def test_mariadb_healthcheck(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.set_test("mariadb")
+ output = render.render()
+ assert (
+ output["services"]["test_container"]["healthcheck"]["test"]
+ == "mariadb-admin --user=root --host=127.0.0.1 --port=3306 --password=$$MARIADB_ROOT_PASSWORD ping"
+ )
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_labels.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_labels.py
new file mode 100644
index 0000000000..ffa21eceac
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_labels.py
@@ -0,0 +1,88 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_disallowed_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.labels.add_label("com.docker.compose.service", "test_service")
+
+
+def test_add_duplicate_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label", "test_value")
+ with pytest.raises(Exception):
+ c1.labels.add_label("my.custom.label", "test_value1")
+
+
+def test_add_label_on_non_existing_container(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_label(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.labels.add_label("my.custom.label1", "test_value1")
+ c1.labels.add_label("my.custom.label2", "test_value2")
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+
+
+def test_auto_add_labels(mock_values):
+ mock_values["labels"] = [
+ {
+ "key": "my.custom.label1",
+ "value": "test_value1",
+ "containers": ["test_container", "test_container2"],
+ },
+ {
+ "key": "my.custom.label2",
+ "value": "test_value2",
+ "containers": ["test_container"],
+ },
+ ]
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c2 = render.add_container("test_container2", "test_image")
+ c1.healthcheck.disable()
+ c2.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ "my.custom.label2": "test_value2",
+ }
+ assert output["services"]["test_container2"]["labels"] == {
+ "my.custom.label1": "test_value1",
+ }
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_notes.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_notes.py
new file mode 100644
index 0000000000..3bdfe33c74
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_notes.py
@@ -0,0 +1,184 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "ix_context": {
+ "app_metadata": {
+ "name": "test_app",
+ "title": "Test App",
+ "train": "enterprise",
+ }
+ },
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_notes(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_on_non_enterprise_train(mock_values):
+ mock_values["ix_context"]["app_metadata"]["train"] = "community"
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://github.com/truenas/apps
+
+"""
+ )
+
+
+def test_notes_with_warnings(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_deprecations(mock_values):
+ render = Render(mock_values)
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_with_body(mock_values):
+ render = Render(mock_values)
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
+
+
+def test_notes_all(mock_values):
+ render = Render(mock_values)
+ render.notes.add_warning("this is not properly configured. fix it now!")
+ render.notes.add_warning("that is not properly configured. fix it later!")
+ render.notes.add_deprecation("this is will be removed later. fix it now!")
+ render.notes.add_deprecation("that is will be removed later. fix it later!")
+ render.notes.set_body(
+ """## Additional info
+
+Some info
+some other info.
+"""
+ )
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert (
+ output["x-notes"]
+ == """# Test App
+
+## Warnings
+
+- this is not properly configured. fix it now!
+- that is not properly configured. fix it later!
+
+## Deprecations
+
+- this is will be removed later. fix it now!
+- that is will be removed later. fix it later!
+
+## Additional info
+
+Some info
+some other info.
+
+## Bug Reports and Feature Requests
+
+If you find a bug in this app or have an idea for a new feature, please file an issue at
+https://ixsystems.atlassian.net
+
+"""
+ )
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_portal.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_portal.py
new file mode 100644
index 0000000000..aebd9425c9
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_portal.py
@@ -0,0 +1,75 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_no_portals(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == []
+
+
+def test_add_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ render.portals.add_portal({"name": "Other Portal", "scheme": "https", "path": "/", "port": 8443})
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["x-portals"] == [
+ {"name": "Other Portal", "scheme": "https", "host": "0.0.0.0", "port": 8443, "path": "/"},
+ {"name": "Web UI", "scheme": "http", "host": "0.0.0.0", "port": 8080, "path": "/"},
+ ]
+
+
+def test_add_duplicate_portal(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_duplicate_portal_with_explicit_name(mock_values):
+ render = Render(mock_values)
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+ with pytest.raises(Exception):
+ render.portals.add_portal({"name": "Some Portal", "scheme": "http", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_scheme(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "invalid_scheme", "path": "/", "port": 8080})
+
+
+def test_add_portal_with_invalid_path(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "invalid_path", "port": 8080})
+
+
+def test_add_portal_with_invalid_path_double_slash(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/some//path", "port": 8080})
+
+
+def test_add_portal_with_invalid_port(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.portals.add_portal({"scheme": "http", "path": "/", "port": -1})
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_ports.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_ports.py
new file mode 100644
index 0000000000..7b37a03600
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_ports.py
@@ -0,0 +1,209 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+tests = [
+ {
+ "name": "add_ports_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8082, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "add_duplicate_ports_should_fail",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_different_protocol_should_work",
+ "inputs": [
+ {"values": (8081, 8080), "expect_error": False},
+ {"values": (8081, 8080, {"protocol": "udp"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_same_port_for_both_wildcard_families_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {
+ "published": 8081,
+ "target": 8080,
+ "protocol": "tcp",
+ "mode": "ingress",
+ "host_ip": "fd00:1234:5678:abcd::10",
+ },
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_port_with_different_v4_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ ],
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_protocol_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"protocol": "invalid_protocol"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_mode_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"mode": "invalid_mode"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_ip_should_fail",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "invalid_ip"}), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_host_port_should_fail",
+ "inputs": [
+ {"values": (-1, 8080), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_port_with_invalid_container_port_should_fail",
+ "inputs": [
+ {"values": (8081, -1), "expect_error": True},
+ ],
+ },
+ {
+ "name": "adding_duplicate_ports_with_different_host_ip_should_work",
+ "inputs": [
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.10", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "192.168.1.11", "protocol": "udp"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False},
+ {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::11"}), "expect_error": False},
+ ],
+ # fmt: off
+ "expected": [
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"},
+ {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"},
+ ],
+ # fmt: on
+ },
+]
+
+
+@pytest.mark.parametrize("test", tests)
+def test_ports(test):
+ mock_values = {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+
+ errored = False
+ for input in test["inputs"]:
+ if input["expect_error"]:
+ with pytest.raises(Exception):
+ c1.ports.add_port(*input["values"])
+ errored = True
+ else:
+ c1.ports.add_port(*input["values"])
+
+ errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False
+ if errored:
+ return
+
+ output = render.render()
+ assert output["services"]["test_container"]["ports"] == test["expected"]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_render.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_render.py
new file mode 100644
index 0000000000..60dc00679e
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_render.py
@@ -0,0 +1,37 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_values_cannot_be_modified(mock_values):
+ render = Render(mock_values)
+ render.values["test"] = "test"
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_duplicate_containers(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_no_containers(mock_values):
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_resources.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_resources.py
new file mode 100644
index 0000000000..cd83d164e5
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_resources.py
@@ -0,0 +1,140 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_automatically_add_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": 1.0}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1.0"
+
+
+def test_invalid_cpu(mock_values):
+ mock_values["resources"] = {"limits": {"cpus": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": 1024}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "1024M"
+
+
+def test_invalid_memory(mock_values):
+ mock_values["resources"] = {"limits": {"memory": "invalid"}}
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_automatically_add_gpus(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ output = render.render()
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_0", "uuid_1"],
+ }
+ assert output["services"]["test_container"]["group_add"] == [44, 107, 568]
+
+
+def test_gpu_without_uuid(mock_values):
+ mock_values["resources"] = {
+ "gpus": {
+ "nvidia_gpu_selection": {
+ "pci_slot_0": {"uuid": "", "use_gpu": True},
+ "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True},
+ },
+ }
+ }
+ render = Render(mock_values)
+ with pytest.raises(Exception):
+ render.add_container("test_container", "test_image")
+
+
+def test_remove_cpus_and_memory_with_gpus(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_1", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "limits" not in output["services"]["test_container"]["deploy"]["resources"]
+ devices = output["services"]["test_container"]["deploy"]["resources"]["reservations"]["devices"]
+ assert len(devices) == 1
+ assert devices[0] == {
+ "capabilities": ["gpu"],
+ "driver": "nvidia",
+ "device_ids": ["uuid_1"],
+ }
+
+
+def test_remove_cpus_and_memory(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_cpus_and_memory()
+ output = render.render()
+ assert "deploy" not in output["services"]["test_container"]
+
+
+def test_remove_devices(mock_values):
+ mock_values["resources"] = {"gpus": {"nvidia_gpu_selection": {"pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}}}}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.remove_devices()
+ output = render.render()
+ assert "reservations" not in output["services"]["test_container"]["deploy"]["resources"]
+ assert output["services"]["test_container"]["group_add"] == [568]
+
+
+def test_set_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.deploy.resources.set_profile("low")
+ output = render.render()
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["cpus"] == "1"
+ assert output["services"]["test_container"]["deploy"]["resources"]["limits"]["memory"] == "512M"
+
+
+def test_set_profile_invalid_profile(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.deploy.resources.set_profile("invalid_profile")
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_restart.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_restart.py
new file mode 100644
index 0000000000..06b2975590
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_restart.py
@@ -0,0 +1,57 @@
+import pytest
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_invalid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("invalid_policy")
+
+
+def test_valid_restart_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure")
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure"
+
+
+def test_valid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.restart.set_policy("on-failure", 10)
+ output = render.render()
+ assert output["services"]["test_container"]["restart"] == "on-failure:10"
+
+
+def test_invalid_restart_policy_with_maximum_retry_count(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("on-failure", maximum_retry_count=-1)
+
+
+def test_invalid_restart_policy_with_maximum_retry_count_and_policy(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.restart.set_policy("always", maximum_retry_count=10)
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_sysctls.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_sysctls.py
new file mode 100644
index 0000000000..c9414044ea
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_sysctls.py
@@ -0,0 +1,62 @@
+import pytest
+
+
+from render import Render
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ c1.sysctls.add("fs.mqueue.msg_max", 100)
+ output = render.render()
+ assert output["services"]["test_container"]["sysctls"] == {"net.ipv4.ip_forward": "1", "fs.mqueue.msg_max": "100"}
+
+
+def test_add_net_sysctl_with_host_network(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.set_network_mode("host")
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ render.render()
+
+
+def test_add_duplicate_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("net.ipv4.ip_forward", 1)
+ with pytest.raises(Exception):
+ c1.sysctls.add("net.ipv4.ip_forward", 0)
+
+
+def test_add_empty_sysctl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.sysctls.add("", 1)
+
+
+def test_add_sysctl_with_invalid_key(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.sysctls.add("invalid.sysctl", 1)
+ with pytest.raises(Exception):
+ render.render()
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_validations.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_validations.py
new file mode 100644
index 0000000000..f0986ce9a5
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/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/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_volumes.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_volumes.py
new file mode 100644
index 0000000000..9a98956bdc
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/tests/test_volumes.py
@@ -0,0 +1,727 @@
+import pytest
+
+
+from render import Render
+from formatter import get_hashed_name_for_volume
+
+
+@pytest.fixture
+def mock_values():
+ return {
+ "images": {
+ "test_image": {
+ "repository": "nginx",
+ "tag": "latest",
+ }
+ },
+ }
+
+
+def test_add_volume_invalid_type(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "invalid_type"})
+
+
+def test_add_volume_empty_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ with pytest.raises(Exception):
+ c1.add_storage("", {"type": "tmpfs"})
+
+
+def test_add_volume_duplicate_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", {"type": "tmpfs"})
+
+
+def test_add_volume_host_path_invalid_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "propagation": "invalid_propagation"},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_host_path_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_with_acl_no_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"acl_enable": True, "acl": {"path": ""}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_add_host_path_volume_mount(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_acl(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {
+ "type": "host_path",
+ "host_path_config": {"path": "/mnt/test", "acl_enable": True, "acl": {"path": "/mnt/test/acl"}},
+ }
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test/acl",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_propagation(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "propagation": "slave"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "slave"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_create_host_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test", "create_host_path": True}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_host_path_volume_mount_with_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "read_only": True, "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/some/path", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_invalid_dataset_name(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "invalid_dataset"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_no_ix_volume_config(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", ix_volume_config)
+
+
+def test_add_ix_volume_volume_mount(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {"type": "ix_volume", "ix_volume_config": {"dataset_name": "test_dataset"}}
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_ix_volume_volume_mount_with_options(mock_values):
+ mock_values["ix_volumes"] = {"test_dataset": "/mnt/test"}
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ ix_volume_config = {
+ "type": "ix_volume",
+ "ix_volume_config": {"dataset_name": "test_dataset", "propagation": "rslave", "create_host_path": True},
+ }
+ c1.add_storage("/some/path", ix_volume_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/some/path",
+ "read_only": False,
+ "bind": {"create_host_path": True, "propagation": "rslave"},
+ }
+ ]
+
+
+def test_cifs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"path": "/path", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "username": "user", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_username(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "password": "password"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_missing_password(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs", "cifs_config": {"server": "server", "path": "/path", "username": "user"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_without_cifs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {"type": "cifs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["verbose=true", "verbose=true"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["user=username"],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": {"verbose": True},
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_cifs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_config = {
+ "type": "cifs",
+ "cifs_config": {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": [{"verbose": True}],
+ },
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", cifs_config)
+
+
+def test_add_cifs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {"server": "server", "path": "/path", "username": "user", "password": "pas$word"}
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {"type": "cifs", "device": "//server/path", "o": "noperm,password=pas$$word,user=user"}
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_cifs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ cifs_inner_config = {
+ "server": "server",
+ "path": "/path",
+ "username": "user",
+ "password": "pas$word",
+ "options": ["vers=3.0", "verbose=true"],
+ }
+ cifs_config = {"type": "cifs", "cifs_config": cifs_inner_config}
+ c1.add_storage("/some/path", cifs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("cifs", cifs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "cifs",
+ "device": "//server/path",
+ "o": "noperm,password=pas$$word,user=user,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_missing_server(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"path": "/path"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_missing_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_without_nfs_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs"}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_duplicate_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {
+ "type": "nfs",
+ "nfs_config": {"server": "server", "path": "/path", "options": ["verbose=true", "verbose=true"]},
+ }
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_disallowed_option(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": ["addr=server"]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": {"verbose": True}}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_nfs_volume_invalid_options2(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_config = {"type": "nfs", "nfs_config": {"server": "server", "path": "/path", "options": [{"verbose": True}]}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", nfs_config)
+
+
+def test_add_nfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path"}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {vol_name: {"driver_opts": {"type": "nfs", "device": ":/path", "o": "addr=server"}}}
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_nfs_volume_with_options(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ nfs_inner_config = {"server": "server", "path": "/path", "options": ["vers=3.0", "verbose=true"]}
+ nfs_config = {"type": "nfs", "nfs_config": nfs_inner_config}
+ c1.add_storage("/some/path", nfs_config)
+ output = render.render()
+ vol_name = get_hashed_name_for_volume("nfs", nfs_inner_config)
+ assert output["volumes"] == {
+ vol_name: {
+ "driver_opts": {
+ "type": "nfs",
+ "device": ":/path",
+ "o": "addr=server,verbose=true,vers=3.0",
+ }
+ }
+ }
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "source": vol_name, "target": "/some/path", "read_only": False, "volume": {"nocopy": False}}
+ ]
+
+
+def test_tmpfs_invalid_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": "2"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_zero_size(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"size": 0}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_invalid_mode(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs", "tmpfs_config": {"mode": "invalid"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_tmpfs_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "tmpfs"}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "tmpfs",
+ "target": "/some/path",
+ "read_only": False,
+ }
+ ]
+
+
+def test_temporary_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "temporary", "volume_config": {"volume_name": "test_temp_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "source": "test_temp_volume",
+ "type": "volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+
+
+def test_docker_volume_missing_config(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume_missing_volume_name(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": ""}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", vol_config)
+
+
+def test_docker_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "volume", "volume_config": {"volume_name": "test_volume"}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "volume",
+ "source": "test_volume",
+ "target": "/some/path",
+ "read_only": False,
+ "volume": {"nocopy": False},
+ }
+ ]
+ assert output["volumes"] == {"test_volume": {}}
+
+
+def test_anonymous_volume(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ vol_config = {"type": "anonymous", "volume_config": {"nocopy": True}}
+ c1.add_storage("/some/path", vol_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {"type": "volume", "target": "/some/path", "read_only": False, "volume": {"nocopy": True}}
+ ]
+ assert "volumes" not in output
+
+
+def test_add_udev(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev()
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_udev_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.add_udev(read_only=False)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/run/udev",
+ "target": "/run/udev",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_not_read_only(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(read_only=False, mount_path="/var/run/docker.sock")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/var/run/docker.sock",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_add_docker_socket_mount_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ c1.storage._add_docker_socket(mount_path="/some/path")
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/var/run/docker.sock",
+ "target": "/some/path",
+ "read_only": True,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
+
+
+def test_host_path_with_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt"}}
+ with pytest.raises(Exception):
+ c1.add_storage("/some/path", host_path_config)
+
+
+def test_host_path_without_disallowed_path(mock_values):
+ render = Render(mock_values)
+ c1 = render.add_container("test_container", "test_image")
+ c1.healthcheck.disable()
+ host_path_config = {"type": "host_path", "host_path_config": {"path": "/mnt/test"}}
+ c1.add_storage("/mnt", host_path_config)
+ output = render.render()
+ assert output["services"]["test_container"]["volumes"] == [
+ {
+ "type": "bind",
+ "source": "/mnt/test",
+ "target": "/mnt",
+ "read_only": False,
+ "bind": {"create_host_path": False, "propagation": "rprivate"},
+ }
+ ]
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/validations.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/validations.py
new file mode 100644
index 0000000000..d4aa633006
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/validations.py
@@ -0,0 +1,316 @@
+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_port_bind_mode_or_raise(status: str):
+ valid_statuses = ("published", "exposed", "")
+ if status not in valid_statuses:
+ raise RenderError(f"Invalid port status [{status}]")
+ return status
+
+
+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_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
+ valid_modes = ("", "host", "private", "shareable", "none")
+ if ipc_mode in valid_modes:
+ return ipc_mode
+ if ipc_mode.startswith("container:"):
+ if ipc_mode[10:] not in containers:
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
+ return ipc_mode
+ raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
+
+
+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 valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
+ parts = dev_grp_rule.split(" ")
+ if len(parts) != 3:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [ : ]"
+ )
+
+ valid_types = ("a", "b", "c")
+ if parts[0] not in valid_types:
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
+ f" but got [{parts[0]}]"
+ )
+
+ major, minor = parts[1].split(":")
+ for part in (major, minor):
+ if part != "*" and not part.isdigit():
+ raise RenderError(
+ f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
+ f" or [*] but got [{major}] and [{minor}]"
+ )
+
+ valid_cgroup_perm_or_raise(parts[2])
+
+ return dev_grp_rule
+
+
+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/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount.py
new file mode 100644
index 0000000000..aadd077750
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount.py
@@ -0,0 +1,92 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorage
+
+try:
+ from .error import RenderError
+ from .formatter import merge_dicts_no_overwrite
+ from .volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from .volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+except ImportError:
+ from error import RenderError
+ from formatter import merge_dicts_no_overwrite
+ from volume_mount_types import BindMountType, VolumeMountType, TmpfsMountType
+ from volume_sources import HostPathSource, IxVolumeSource, CifsSource, NfsSource, VolumeSource
+
+
+class VolumeMount:
+ def __init__(self, render_instance: "Render", mount_path: str, config: "IxStorage"):
+ self._render_instance = render_instance
+ self.mount_path: str = mount_path
+
+ storage_type: str = config.get("type", "")
+ if not storage_type:
+ raise RenderError("Expected [type] to be set for volume mounts.")
+
+ match storage_type:
+ case "host_path":
+ spec_type = "bind"
+ mount_config = config.get("host_path_config")
+ if mount_config is None:
+ raise RenderError("Expected [host_path_config] to be set for [host_path] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = HostPathSource(self._render_instance, mount_config).get()
+ case "ix_volume":
+ spec_type = "bind"
+ mount_config = config.get("ix_volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [ix_volume_config] to be set for [ix_volume] type.")
+ mount_type_specific_definition = BindMountType(self._render_instance, mount_config).render()
+ source = IxVolumeSource(self._render_instance, mount_config).get()
+ case "tmpfs":
+ spec_type = "tmpfs"
+ mount_config = config.get("tmpfs_config", {})
+ mount_type_specific_definition = TmpfsMountType(self._render_instance, mount_config).render()
+ source = None
+ case "nfs":
+ spec_type = "volume"
+ mount_config = config.get("nfs_config")
+ if mount_config is None:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = NfsSource(self._render_instance, mount_config).get()
+ case "cifs":
+ spec_type = "volume"
+ mount_config = config.get("cifs_config")
+ if mount_config is None:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = CifsSource(self._render_instance, mount_config).get()
+ case "volume":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [volume] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "temporary":
+ spec_type = "volume"
+ mount_config = config.get("volume_config")
+ if mount_config is None:
+ raise RenderError("Expected [volume_config] to be set for [temporary] type.")
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = VolumeSource(self._render_instance, mount_config).get()
+ case "anonymous":
+ spec_type = "volume"
+ mount_config = config.get("volume_config") or {}
+ mount_type_specific_definition = VolumeMountType(self._render_instance, mount_config).render()
+ source = None
+ case _:
+ raise RenderError(f"Storage type [{storage_type}] is not supported for volume mounts.")
+
+ common_spec = {"type": spec_type, "target": self.mount_path, "read_only": config.get("read_only", False)}
+ if source is not None:
+ common_spec["source"] = source
+ self._render_instance.volumes.add_volume(source, storage_type, mount_config) # type: ignore
+
+ self.volume_mount_spec = merge_dicts_no_overwrite(common_spec, mount_type_specific_definition)
+
+ def render(self) -> dict:
+ return self.volume_mount_spec
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount_types.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount_types.py
new file mode 100644
index 0000000000..00a0ec3a18
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_mount_types.py
@@ -0,0 +1,72 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageTmpfsConfig, IxStorageVolumeConfig, IxStorageBindLikeConfigs
+
+
+try:
+ from .error import RenderError
+ from .validations import valid_host_path_propagation, valid_octal_mode_or_raise
+except ImportError:
+ from error import RenderError
+ from validations import valid_host_path_propagation, valid_octal_mode_or_raise
+
+
+class TmpfsMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageTmpfsConfig"):
+ self._render_instance = render_instance
+ self.spec = {"tmpfs": {}}
+ size = config.get("size", None)
+ mode = config.get("mode", None)
+
+ if size is not None:
+ if not isinstance(size, int):
+ raise RenderError(f"Expected [size] to be an integer for [tmpfs] type, got [{size}]")
+ if not size > 0:
+ raise RenderError(f"Expected [size] to be greater than 0 for [tmpfs] type, got [{size}]")
+ # Convert Mebibytes to Bytes
+ self.spec["tmpfs"]["size"] = size * 1024 * 1024
+
+ if mode is not None:
+ mode = valid_octal_mode_or_raise(mode)
+ self.spec["tmpfs"]["mode"] = int(mode, 8)
+
+ if not self.spec["tmpfs"]:
+ self.spec.pop("tmpfs")
+
+ def render(self) -> dict:
+ """Render the tmpfs mount specification."""
+ return self.spec
+
+
+class BindMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageBindLikeConfigs"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ propagation = valid_host_path_propagation(config.get("propagation", "rprivate"))
+ create_host_path = config.get("create_host_path", False)
+
+ self.spec: dict = {
+ "bind": {
+ "create_host_path": create_host_path,
+ "propagation": propagation,
+ }
+ }
+
+ def render(self) -> dict:
+ """Render the bind mount specification."""
+ return self.spec
+
+
+class VolumeMountType:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.spec: dict = {}
+
+ self.spec: dict = {"volume": {"nocopy": config.get("nocopy", False)}}
+
+ def render(self) -> dict:
+ """Render the volume mount specification."""
+ return self.spec
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_sources.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_sources.py
new file mode 100644
index 0000000000..6aba23df23
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_sources.py
@@ -0,0 +1,110 @@
+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("/")
+ # TODO: Hack for Nextcloud deprecated config. Remove once we remove support for it
+ allow_unsafe_ix_volume = config.get("allow_unsafe_ix_volume", False)
+ self.source = allowed_fs_host_path_or_raise(path, allow_unsafe_ix_volume)
+
+ 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/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_types.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_types.py
new file mode 100644
index 0000000000..4ccea08f83
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volume_types.py
@@ -0,0 +1,133 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+ from storage import IxStorageNfsConfig, IxStorageCifsConfig, IxStorageVolumeConfig
+
+
+try:
+ from .error import RenderError
+ from .formatter import escape_dollar
+ from .validations import valid_fs_path_or_raise
+except ImportError:
+ from error import RenderError
+ from formatter import escape_dollar
+ from validations import valid_fs_path_or_raise
+
+
+class NfsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageNfsConfig"):
+ self._render_instance = render_instance
+
+ if not config:
+ raise RenderError("Expected [nfs_config] to be set for [nfs] type")
+
+ required_keys = ["server", "path"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [nfs] type")
+
+ opts = [f"addr={config['server']}"]
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [nfs_config.options] to be a list for [nfs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["addr"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [nfs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [nfs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [nfs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+
+ opts.sort()
+
+ path = valid_fs_path_or_raise(config["path"].rstrip("/"))
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "nfs",
+ "device": f":{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class CifsVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageCifsConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ if not config:
+ raise RenderError("Expected [cifs_config] to be set for [cifs] type")
+
+ required_keys = ["server", "path", "username", "password"]
+ for key in required_keys:
+ if not config.get(key):
+ raise RenderError(f"Expected [{key}] to be set for [cifs] type")
+
+ opts = [
+ "noperm",
+ f"user={config['username']}",
+ f"password={config['password']}",
+ ]
+
+ domain = config.get("domain")
+ if domain:
+ opts.append(f"domain={domain}")
+
+ cfg_options = config.get("options")
+ if cfg_options:
+ if not isinstance(cfg_options, list):
+ raise RenderError("Expected [cifs_config.options] to be a list for [cifs] type")
+
+ tracked_keys: set[str] = set()
+ disallowed_opts = ["user", "password", "domain", "noperm"]
+ for opt in cfg_options:
+ if not isinstance(opt, str):
+ raise RenderError("Options for [cifs] type must be a list of strings.")
+
+ key = opt.split("=")[0]
+ if key in tracked_keys:
+ raise RenderError(f"Option [{key}] already added for [cifs] type.")
+ if key in disallowed_opts:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ for disallowed in disallowed_opts:
+ if key == disallowed:
+ raise RenderError(f"Option [{key}] is not allowed for [cifs] type.")
+ opts.append(opt)
+ tracked_keys.add(key)
+ opts.sort()
+
+ server = config["server"].lstrip("/")
+ path = config["path"].strip("/")
+ path = valid_fs_path_or_raise("/" + path).lstrip("/")
+
+ self.volume_spec = {
+ "driver_opts": {
+ "type": "cifs",
+ "device": f"//{server}/{path}",
+ "o": f"{','.join([escape_dollar(opt) for opt in opts])}",
+ },
+ }
+
+ def get(self):
+ return self.volume_spec
+
+
+class DockerVolume:
+ def __init__(self, render_instance: "Render", config: "IxStorageVolumeConfig"):
+ self._render_instance = render_instance
+ self.volume_spec: dict = {}
+
+ def get(self):
+ return self.volume_spec
diff --git a/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volumes.py b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volumes.py
new file mode 100644
index 0000000000..e6925a402f
--- /dev/null
+++ b/trains/stable/nextcloud/1.6.0/templates/library/base_v2_1_14/volumes.py
@@ -0,0 +1,66 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from render import Render
+
+
+try:
+ from .error import RenderError
+ from .storage import IxStorageVolumeLikeConfigs
+ from .volume_types import NfsVolume, CifsVolume, DockerVolume
+except ImportError:
+ from error import RenderError
+ from storage import IxStorageVolumeLikeConfigs
+ from volume_types import NfsVolume, CifsVolume, DockerVolume
+
+
+class Volumes:
+ def __init__(self, render_instance: "Render"):
+ self._render_instance = render_instance
+ self._volumes: dict[str, Volume] = {}
+
+ def add_volume(
+ self,
+ source: str,
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ # This method can be called many times from the volume mounts
+ # Only add the volume if it is not already added, but dont raise an error
+ if source == "":
+ raise RenderError(f"Volume source [{source}] cannot be empty")
+
+ if source in self._volumes:
+ return
+
+ self._volumes[source] = Volume(self._render_instance, storage_type, config)
+
+ def has_volumes(self) -> bool:
+ return bool(self._volumes)
+
+ def render(self):
+ return {name: v.render() for name, v in sorted(self._volumes.items()) if v.render() is not None}
+
+
+class Volume:
+ def __init__(
+ self,
+ render_instance: "Render",
+ storage_type: str,
+ config: "IxStorageVolumeLikeConfigs",
+ ):
+ self._render_instance = render_instance
+ self.volume_spec: dict | None = {}
+
+ match storage_type:
+ case "nfs":
+ self.volume_spec = NfsVolume(self._render_instance, config).get() # type: ignore
+ case "cifs":
+ self.volume_spec = CifsVolume(self._render_instance, config).get() # type: ignore
+ case "volume" | "temporary":
+ self.volume_spec = DockerVolume(self._render_instance, config).get() # type: ignore
+ case _:
+ self.volume_spec = None
+
+ def render(self):
+ return self.volume_spec
diff --git a/trains/stable/nextcloud/1.5.18/templates/macros/nc.jinja.conf b/trains/stable/nextcloud/1.6.0/templates/macros/nc.jinja.conf
similarity index 84%
rename from trains/stable/nextcloud/1.5.18/templates/macros/nc.jinja.conf
rename to trains/stable/nextcloud/1.6.0/templates/macros/nc.jinja.conf
index 6bd1f670a8..7da03a164a 100644
--- a/trains/stable/nextcloud/1.5.18/templates/macros/nc.jinja.conf
+++ b/trains/stable/nextcloud/1.6.0/templates/macros/nc.jinja.conf
@@ -11,6 +11,13 @@ max_execution_time={{ values.nextcloud.max_execution_time }}
LimitRequestBody {{ values.nextcloud.php_upload_limit * bytes_gb }}
{%- endmacro -%}
+{% macro use_x_real_ip_in_logs() -%}
+{# `(%{X-Real-IP}i)` is added after each LogFormat `%h` statement from /etc/apache2/apache2.conf -#}
+LogFormat "%v:%p %h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+LogFormat "%h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+LogFormat "%h (%{X-Real-IP}i) %l %u %t \"%r\" %>s %O" common
+{%- endmacro -%}
+
{% macro nginx_conf(values) -%}
{%- set port = namespace(x=":$server_port") -%}
{%- if values.network.nginx.use_different_port -%}
@@ -35,6 +42,7 @@ http {
client_max_body_size {{ values.nextcloud.php_upload_limit }}G;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains; preload" always;
+
location = /robots.txt {
allow all;
log_not_found off;
@@ -70,6 +78,8 @@ http {
proxy_send_timeout {{ values.network.nginx.proxy_timeout }}s;
proxy_read_timeout {{ values.network.nginx.proxy_timeout }}s;
}
+
+ include /etc/nginx/includes/*.conf;
}
}
{%- endmacro -%}
diff --git a/trains/stable/nextcloud/1.5.18/templates/macros/nc.jinja.sh b/trains/stable/nextcloud/1.6.0/templates/macros/nc.jinja.sh
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/macros/nc.jinja.sh
rename to trains/stable/nextcloud/1.6.0/templates/macros/nc.jinja.sh
diff --git a/trains/stable/nextcloud/1.5.18/templates/test_values/basic-values.yaml b/trains/stable/nextcloud/1.6.0/templates/test_values/basic-values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/test_values/basic-values.yaml
rename to trains/stable/nextcloud/1.6.0/templates/test_values/basic-values.yaml
diff --git a/trains/stable/nextcloud/1.5.18/templates/test_values/https-values.yaml b/trains/stable/nextcloud/1.6.0/templates/test_values/https-values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/test_values/https-values.yaml
rename to trains/stable/nextcloud/1.6.0/templates/test_values/https-values.yaml
diff --git a/trains/stable/nextcloud/1.5.18/templates/test_values/imaginary-values.yaml b/trains/stable/nextcloud/1.6.0/templates/test_values/imaginary-values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/test_values/imaginary-values.yaml
rename to trains/stable/nextcloud/1.6.0/templates/test_values/imaginary-values.yaml
diff --git a/trains/stable/nextcloud/1.5.18/templates/test_values/no-cron-values.yaml b/trains/stable/nextcloud/1.6.0/templates/test_values/no-cron-values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/test_values/no-cron-values.yaml
rename to trains/stable/nextcloud/1.6.0/templates/test_values/no-cron-values.yaml
diff --git a/trains/stable/nextcloud/1.5.18/templates/test_values/same-vol-values.yaml b/trains/stable/nextcloud/1.6.0/templates/test_values/same-vol-values.yaml
similarity index 100%
rename from trains/stable/nextcloud/1.5.18/templates/test_values/same-vol-values.yaml
rename to trains/stable/nextcloud/1.6.0/templates/test_values/same-vol-values.yaml
diff --git a/trains/stable/nextcloud/app_versions.json b/trains/stable/nextcloud/app_versions.json
index 105489b6a0..64994d6eae 100644
--- a/trains/stable/nextcloud/app_versions.json
+++ b/trains/stable/nextcloud/app_versions.json
@@ -1,13 +1,13 @@
{
- "1.5.18": {
+ "1.6.0": {
"healthy": true,
"supported": true,
"healthy_error": null,
- "location": "/__w/apps/apps/trains/stable/nextcloud/1.5.18",
- "last_update": "2025-01-30 08:03:00",
+ "location": "/__w/apps/apps/trains/stable/nextcloud/1.6.0",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
- "human_version": "30.0.5_1.5.18",
- "version": "1.5.18",
+ "human_version": "30.0.5_1.6.0",
+ "version": "1.6.0",
"app_metadata": {
"app_version": "30.0.5",
"capabilities": [
@@ -118,7 +118,7 @@
],
"title": "Nextcloud",
"train": "stable",
- "version": "1.5.18"
+ "version": "1.6.0"
},
"schema": {
"groups": [
@@ -542,6 +542,25 @@
],
"required": true
}
+ },
+ {
+ "variable": "custom_confs",
+ "label": "Custom Nginx Configurations",
+ "description": "List of custom Nginx configurations.",
+ "schema": {
+ "type": "list",
+ "default": [],
+ "items": [
+ {
+ "variable": "conf",
+ "label": "Configuration",
+ "schema": {
+ "type": "hostpath",
+ "required": true
+ }
+ }
+ ]
+ }
}
]
}
diff --git a/trains/stable/photoprism/app_versions.json b/trains/stable/photoprism/app_versions.json
index 07eb6322ae..ac859c34bc 100644
--- a/trains/stable/photoprism/app_versions.json
+++ b/trains/stable/photoprism/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/photoprism/1.2.9",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "240915_1.2.9",
"version": "1.2.9",
diff --git a/trains/stable/pihole/app_versions.json b/trains/stable/pihole/app_versions.json
index 3127e519b2..fda9d70614 100644
--- a/trains/stable/pihole/app_versions.json
+++ b/trains/stable/pihole/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/pihole/1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "2024.07.0_1.2.7",
"version": "1.2.7",
diff --git a/trains/stable/plex/app_versions.json b/trains/stable/plex/app_versions.json
index 69fe6c07c9..b2891b1aa7 100644
--- a/trains/stable/plex/app_versions.json
+++ b/trains/stable/plex/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/plex/1.1.14",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.41.3.9314-a0bfb8370_1.1.14",
"version": "1.1.14",
diff --git a/trains/stable/prometheus/app_versions.json b/trains/stable/prometheus/app_versions.json
index 09e971a99a..1c209e6cca 100644
--- a/trains/stable/prometheus/app_versions.json
+++ b/trains/stable/prometheus/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/prometheus/1.2.8",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v3.1.0_1.2.8",
"version": "1.2.8",
diff --git a/trains/stable/storj/app_versions.json b/trains/stable/storj/app_versions.json
index 64100093bf..e03b57f8ea 100644
--- a/trains/stable/storj/app_versions.json
+++ b/trains/stable/storj/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/storj/1.2.7",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "6f87ea801-v1.71.2-go1.18.8_1.2.7",
"version": "1.2.7",
diff --git a/trains/stable/syncthing/app_versions.json b/trains/stable/syncthing/app_versions.json
index 3b9bb9b4b8..92706201c6 100644
--- a/trains/stable/syncthing/app_versions.json
+++ b/trains/stable/syncthing/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/syncthing/1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "1.29.2_1.1.11",
"version": "1.1.11",
diff --git a/trains/stable/wg-easy/app_versions.json b/trains/stable/wg-easy/app_versions.json
index a98c4e6dfb..3268aceb43 100644
--- a/trains/stable/wg-easy/app_versions.json
+++ b/trains/stable/wg-easy/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/stable/wg-easy/1.1.11",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "14_1.1.11",
"version": "1.1.11",
diff --git a/trains/test/ix-remote-assist/app_versions.json b/trains/test/ix-remote-assist/app_versions.json
index 8e7058c546..09da41c611 100644
--- a/trains/test/ix-remote-assist/app_versions.json
+++ b/trains/test/ix-remote-assist/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/ix-remote-assist/1.0.2",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1.76.6_1.0.2",
"version": "1.0.2",
diff --git a/trains/test/nginx/app_versions.json b/trains/test/nginx/app_versions.json
index a022ec7c7c..26c230eab4 100644
--- a/trains/test/nginx/app_versions.json
+++ b/trains/test/nginx/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/nginx/1.0.6",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.6",
"version": "1.0.6",
@@ -107,7 +107,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/nginx/1.0.5",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.5",
"version": "1.0.5",
@@ -200,7 +200,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/nginx/1.0.4",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.4",
"version": "1.0.4",
@@ -293,7 +293,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/nginx/1.0.3",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.3",
"version": "1.0.3",
diff --git a/trains/test/other-nginx/app_versions.json b/trains/test/other-nginx/app_versions.json
index b6de9cb2df..346ccd2c84 100644
--- a/trains/test/other-nginx/app_versions.json
+++ b/trains/test/other-nginx/app_versions.json
@@ -4,7 +4,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/other-nginx/1.0.1",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.1",
"version": "1.0.1",
@@ -97,7 +97,7 @@
"supported": true,
"healthy_error": null,
"location": "/__w/apps/apps/trains/test/other-nginx/1.0.0",
- "last_update": "2025-01-30 08:03:00",
+ "last_update": "2025-01-31 17:33:02",
"required_features": [],
"human_version": "v1_1.0.0",
"version": "1.0.0",