mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
feat: add dynamic template registry and smart volume path sanitizer
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user