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",