feat: add dynamic template registry and smart volume path sanitizer

This commit is contained in:
SaelixCode
2026-03-05 10:15:54 -05:00
parent 47775c6dd2
commit 536a714d9b
2 changed files with 37 additions and 7 deletions
+2
View File
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- **Added:** Dynamic Template Registry URL support via global settings, defaulting to LinuxServer.io templates.
- **Fixed:** Smart Volume Sanitizer to automatically rewrite messy Portainer bind mounts into clean, relative paths (Sencho 1:1 path rule).
- Git Flow branching strategy and branch protection. - Git Flow branching strategy and branch protection.
- GitHub Actions CI pipeline for automated TypeScript build verification. - GitHub Actions CI pipeline for automated TypeScript build verification.
- **Added:** Automated Docker Hub CI/CD pipeline for the `dev` and `latest` tags. - **Added:** Automated Docker Hub CI/CD pipeline for the `dev` and `latest` tags.
+35 -7
View File
@@ -1,4 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { DatabaseService } from './DatabaseService';
export interface TemplateEnv { export interface TemplateEnv {
name: string; name: string;
@@ -38,7 +40,6 @@ export class TemplateService {
private cachedTemplates: Template[] = []; private cachedTemplates: Template[] = [];
private lastFetchTime: number = 0; private lastFetchTime: number = 0;
private readonly CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours private readonly CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
private readonly TEMPLATES_URL = 'https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json';
public async getTemplates(): Promise<Template[]> { public async getTemplates(): Promise<Template[]> {
const now = Date.now(); const now = Date.now();
@@ -47,7 +48,11 @@ export class TemplateService {
} }
try { try {
const response = await axios.get<TemplatesResponse>(this.TEMPLATES_URL); const settings = DatabaseService.getInstance().getGlobalSettings();
// Default to a reliable LSIO Portainer v2 template registry if not set
const registryUrl = settings.template_registry_url || 'https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json';
const response = await axios.get<TemplatesResponse>(registryUrl);
// Filter out templates without images as we are generating compose files from image, ports, etc. // Filter out templates without images as we are generating compose files from image, ports, etc.
this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1); this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1);
this.lastFetchTime = now; this.lastFetchTime = now;
@@ -80,14 +85,37 @@ export class TemplateService {
if (template.volumes && template.volumes.length > 0) { if (template.volumes && template.volumes.length > 0) {
yaml += ` volumes:\n`; yaml += ` volumes:\n`;
for (const vol of template.volumes) { for (const vol of template.volumes) {
let hostPath = '';
let containerPath = '';
let options = '';
if (typeof vol === 'string') { if (typeof vol === 'string') {
yaml += ` - ${vol}\n`; const parts = vol.split(':');
if (parts.length === 1) {
yaml += ` - ${vol}\n`;
continue;
}
hostPath = parts[0];
containerPath = parts[1];
options = parts.slice(2).join(':');
if (options) options = `:${options}`;
} else if (vol.container) { } else if (vol.container) {
// If bind is not provided, we extract the folder name from the container path containerPath = vol.container;
const containerFolder = vol.container.split('/').pop() || 'data'; const containerFolder = containerPath.split('/').filter(Boolean).pop() || 'data';
const localBind = vol.bind ? vol.bind.replace(/^\//, './') : `./${containerFolder}`; hostPath = vol.bind ? vol.bind : `./${containerFolder}`;
yaml += ` - ${localBind}:${vol.container}${vol.readonly ? ':ro' : ''}\n`; options = vol.readonly ? ':ro' : '';
} else {
continue;
} }
if (hostPath.includes('portainer/Files') || hostPath.includes('/your/') || hostPath.includes('/path/to/')) {
const containerFolder = containerPath.split('/').filter(Boolean).pop() || 'data';
hostPath = `./${containerFolder}`;
} else if (hostPath.startsWith('/')) {
hostPath = hostPath.replace(/^\//, './');
}
yaml += ` - ${hostPath}:${containerPath}${options}\n`;
} }
} }