Sync DNS2 production changes - removed obsolete test suite and refactored structure
This commit is contained in:
@@ -62,7 +62,7 @@ module.exports = function(ctx, helpers) {
|
||||
ctx.log.info('deploy', 'DashCA: Using existing index.html');
|
||||
}
|
||||
|
||||
ctx.log.info('deploy', `DashCA: For full features, copy certificate files to ${ destPath}`);
|
||||
ctx.log.info('deploy', 'DashCA: For full features, copy certificate files to ' + destPath);
|
||||
ctx.log.info('deploy', 'DashCA: Static site deployment completed successfully');
|
||||
} catch (error) {
|
||||
ctx.log.error('deploy', 'DashCA deployment error', { error: error.message });
|
||||
@@ -121,14 +121,14 @@ module.exports = function(ctx, helpers) {
|
||||
PortBindings: {},
|
||||
Binds: translatedVolumes,
|
||||
RestartPolicy: { Name: 'unless-stopped' },
|
||||
LogConfig: DOCKER.LOG_CONFIG,
|
||||
LogConfig: DOCKER.LOG_CONFIG
|
||||
},
|
||||
Env: Object.entries(processedTemplate.docker.environment || {}).map(([k, v]) => `${k}=${v}`),
|
||||
Labels: {
|
||||
'sami.managed': 'true', 'sami.app': appId,
|
||||
'sami.subdomain': userConfig.subdomain,
|
||||
'sami.deployed': new Date().toISOString(),
|
||||
},
|
||||
'sami.deployed': new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
processedTemplate.docker.ports.forEach(portMapping => {
|
||||
@@ -164,7 +164,7 @@ module.exports = function(ctx, helpers) {
|
||||
try {
|
||||
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } });
|
||||
if (pruneResult.SpaceReclaimed > 0) {
|
||||
ctx.log.info('docker', 'Pruned dangling images after deploy', { spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) }MB` });
|
||||
ctx.log.info('docker', 'Pruned dangling images after deploy', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
||||
}
|
||||
} catch (pruneErr) {
|
||||
ctx.log.debug('docker', 'Image prune after deploy failed', { error: pruneErr.message });
|
||||
@@ -324,7 +324,7 @@ module.exports = function(ctx, helpers) {
|
||||
tailscaleOnly: config.tailscaleOnly || false,
|
||||
allowedIPs: config.allowedIPs || [],
|
||||
customVolumes: config.customVolumes || undefined,
|
||||
useExisting: false,
|
||||
useExisting: false
|
||||
},
|
||||
container: template.isStaticSite ? null : {
|
||||
image: processedTemplate.docker.image,
|
||||
@@ -340,14 +340,14 @@ module.exports = function(ctx, helpers) {
|
||||
}
|
||||
return env;
|
||||
})(),
|
||||
capabilities: processedTemplate.docker.capabilities || undefined,
|
||||
capabilities: processedTemplate.docker.capabilities || undefined
|
||||
},
|
||||
caddy: {
|
||||
tailscaleOnly: config.tailscaleOnly || false,
|
||||
allowedIPs: config.allowedIPs || [],
|
||||
subpathSupport: template.subpathSupport || 'strip',
|
||||
routingMode: ctx.siteConfig.routingMode,
|
||||
},
|
||||
routingMode: ctx.siteConfig.routingMode
|
||||
}
|
||||
};
|
||||
|
||||
await ctx.addServiceToConfig({
|
||||
@@ -358,7 +358,7 @@ module.exports = function(ctx, helpers) {
|
||||
tailscaleOnly: config.tailscaleOnly || false,
|
||||
routingMode: ctx.siteConfig.routingMode,
|
||||
deployedAt: new Date().toISOString(),
|
||||
deploymentManifest,
|
||||
deploymentManifest
|
||||
});
|
||||
ctx.log.info('deploy', 'Service added to dashboard', { subdomain: config.subdomain });
|
||||
|
||||
@@ -366,7 +366,7 @@ module.exports = function(ctx, helpers) {
|
||||
success: true, containerId, usedExisting,
|
||||
url: serviceUrl,
|
||||
message: usedExisting ? `${template.name} configured using existing container!` : `${template.name} deployed successfully!`,
|
||||
setupInstructions: template.setupInstructions || [],
|
||||
setupInstructions: template.setupInstructions || []
|
||||
};
|
||||
if (dnsWarning) response.warning = dnsWarning;
|
||||
|
||||
|
||||
@@ -38,16 +38,16 @@ module.exports = function(ctx) {
|
||||
const templateImage = template.docker.image.split(':')[0];
|
||||
for (const container of containers) {
|
||||
const containerImage = container.Image.split(':')[0];
|
||||
if (containerImage === templateImage || containerImage.endsWith(`/${ templateImage}`)) {
|
||||
if (containerImage === templateImage || containerImage.endsWith('/' + templateImage)) {
|
||||
const ports = container.Ports.filter(p => p.PublicPort).map(p => ({
|
||||
hostPort: p.PublicPort, containerPort: p.PrivatePort, protocol: p.Type,
|
||||
hostPort: p.PublicPort, containerPort: p.PrivatePort, protocol: p.Type
|
||||
}));
|
||||
return {
|
||||
id: container.Id, shortId: container.Id.slice(0, 12),
|
||||
name: container.Names[0]?.replace(/^\//, '') || 'unknown',
|
||||
image: container.Image, status: container.Status, state: container.State,
|
||||
ports, primaryPort: ports.length > 0 ? ports[0].hostPort : null,
|
||||
labels: container.Labels || {},
|
||||
labels: container.Labels || {}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ module.exports = function(ctx) {
|
||||
'{{PORT}}': config.port || template.defaultPort,
|
||||
'{{MEDIA_PATH}}': mediaPaths[0] || '/media',
|
||||
'{{TIMEZONE}}': ctx.siteConfig.timezone || 'UTC',
|
||||
'{{GENERATED_SECRET}}': crypto.randomBytes(32).toString('hex'),
|
||||
'{{GENERATED_SECRET}}': crypto.randomBytes(32).toString('hex')
|
||||
};
|
||||
|
||||
function replaceInObject(obj) {
|
||||
@@ -117,7 +117,7 @@ module.exports = function(ctx) {
|
||||
const basePath = `/${config.subdomain}`;
|
||||
// Some apps need the full URL, not just the path
|
||||
if (['GF_SERVER_ROOT_URL', 'GITEA__server__ROOT_URL'].includes(template.urlBaseEnv)) {
|
||||
processed.docker.environment[template.urlBaseEnv] = `${ctx.buildServiceUrl(config.subdomain) }/`;
|
||||
processed.docker.environment[template.urlBaseEnv] = ctx.buildServiceUrl(config.subdomain) + '/';
|
||||
} else {
|
||||
processed.docker.environment[template.urlBaseEnv] = basePath;
|
||||
}
|
||||
@@ -137,7 +137,7 @@ module.exports = function(ctx) {
|
||||
config.mediaPath.split(',').map(p => p.trim()).filter(Boolean).forEach(p => allowedRoots.push(path.resolve(p)));
|
||||
}
|
||||
const isAllowed = allowedRoots.some(root =>
|
||||
normalizedHost === root || normalizedHost.startsWith(root + path.sep),
|
||||
normalizedHost === root || normalizedHost.startsWith(root + path.sep)
|
||||
);
|
||||
if (!isAllowed) {
|
||||
ctx.log.warn('deploy', 'Custom volume host path rejected', { hostPath: override.hostPath, allowed: allowedRoots });
|
||||
@@ -162,76 +162,76 @@ module.exports = function(ctx) {
|
||||
c += ` root * ${sitePath}\n\n`;
|
||||
|
||||
if (tailscaleOnly) {
|
||||
c += ' @blocked not remote_ip 100.64.0.0/10\n';
|
||||
c += ' respond @blocked "Access denied. Tailscale connection required." 403\n\n';
|
||||
c += ` @blocked not remote_ip 100.64.0.0/10\n`;
|
||||
c += ` respond @blocked "Access denied. Tailscale connection required." 403\n\n`;
|
||||
}
|
||||
|
||||
if (apiProxy) {
|
||||
c += ' handle /api/* {\n';
|
||||
c += ` handle /api/* {\n`;
|
||||
c += ` reverse_proxy ${apiProxy}\n`;
|
||||
c += ' }\n\n';
|
||||
c += ` }\n\n`;
|
||||
}
|
||||
|
||||
c += ' @crt path *.crt\n';
|
||||
c += ' handle @crt {\n';
|
||||
c += ' header Content-Type application/x-x509-ca-cert\n';
|
||||
c += ' header Content-Disposition "attachment; filename=\\"{file}\\""\n';
|
||||
c += ' header Cache-Control "public, max-age=86400"\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' @der path *.der\n';
|
||||
c += ' handle @der {\n';
|
||||
c += ' header Content-Type application/x-x509-ca-cert\n';
|
||||
c += ' header Content-Disposition "attachment; filename=\\"{file}\\""\n';
|
||||
c += ' header Cache-Control "public, max-age=86400"\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' @mobileconfig path *.mobileconfig\n';
|
||||
c += ' handle @mobileconfig {\n';
|
||||
c += ' header Content-Type application/x-apple-aspen-config\n';
|
||||
c += ' header Content-Disposition "attachment; filename=\\"{file}\\""\n';
|
||||
c += ' header Cache-Control "public, max-age=86400"\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' @ps1 path *.ps1\n';
|
||||
c += ' handle @ps1 {\n';
|
||||
c += ' header Content-Type text/plain\n';
|
||||
c += ' header Content-Disposition "attachment; filename=\\"{file}\\""\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' @sh path *.sh\n';
|
||||
c += ' handle @sh {\n';
|
||||
c += ' header Content-Type text/x-shellscript\n';
|
||||
c += ' header Content-Disposition "attachment; filename=\\"{file}\\""\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' # Static site with SPA fallback\n';
|
||||
c += ' handle {\n';
|
||||
c += ' @notFile not file {path}\n';
|
||||
c += ' rewrite @notFile /index.html\n';
|
||||
c += ' file_server\n';
|
||||
c += ' }\n\n';
|
||||
c += ' # No cache for HTML\n';
|
||||
c += ' @htmlfiles {\n';
|
||||
c += ' path *.html\n';
|
||||
c += ' path /\n';
|
||||
c += ' }\n';
|
||||
c += ' header @htmlfiles Cache-Control "no-store"\n';
|
||||
c += ` @crt path *.crt\n`;
|
||||
c += ` handle @crt {\n`;
|
||||
c += ` header Content-Type application/x-x509-ca-cert\n`;
|
||||
c += ` header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
|
||||
c += ` header Cache-Control "public, max-age=86400"\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` @der path *.der\n`;
|
||||
c += ` handle @der {\n`;
|
||||
c += ` header Content-Type application/x-x509-ca-cert\n`;
|
||||
c += ` header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
|
||||
c += ` header Cache-Control "public, max-age=86400"\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` @mobileconfig path *.mobileconfig\n`;
|
||||
c += ` handle @mobileconfig {\n`;
|
||||
c += ` header Content-Type application/x-apple-aspen-config\n`;
|
||||
c += ` header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
|
||||
c += ` header Cache-Control "public, max-age=86400"\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` @ps1 path *.ps1\n`;
|
||||
c += ` handle @ps1 {\n`;
|
||||
c += ` header Content-Type text/plain\n`;
|
||||
c += ` header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` @sh path *.sh\n`;
|
||||
c += ` handle @sh {\n`;
|
||||
c += ` header Content-Type text/x-shellscript\n`;
|
||||
c += ` header Content-Disposition "attachment; filename=\\"{file}\\""\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` # Static site with SPA fallback\n`;
|
||||
c += ` handle {\n`;
|
||||
c += ` @notFile not file {path}\n`;
|
||||
c += ` rewrite @notFile /index.html\n`;
|
||||
c += ` file_server\n`;
|
||||
c += ` }\n\n`;
|
||||
c += ` # No cache for HTML\n`;
|
||||
c += ` @htmlfiles {\n`;
|
||||
c += ` path *.html\n`;
|
||||
c += ` path /\n`;
|
||||
c += ` }\n`;
|
||||
c += ` header @htmlfiles Cache-Control "no-store"\n`;
|
||||
return c;
|
||||
}
|
||||
|
||||
// HTTPS block
|
||||
let config = `${domain} {\n`;
|
||||
config += ' tls internal\n\n';
|
||||
config += ` tls internal\n\n`;
|
||||
config += siteBlockContent();
|
||||
config += '}';
|
||||
config += `}`;
|
||||
|
||||
// HTTP companion block for devices that haven't trusted the CA yet
|
||||
if (httpAccess) {
|
||||
config += '\n\n# HTTP access for first-time certificate installation\n';
|
||||
config += `\n\n# HTTP access for first-time certificate installation\n`;
|
||||
config += `http://${domain} {\n`;
|
||||
config += siteBlockContent();
|
||||
config += '}';
|
||||
config += `}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -254,7 +254,7 @@ module.exports = function(ctx) {
|
||||
} else if (healthPath && port && httpCheckFailed < 5) {
|
||||
try {
|
||||
const response = await ctx.fetchT(`http://localhost:${port}${healthPath}`, {
|
||||
signal: AbortSignal.timeout(3000), redirect: 'manual',
|
||||
signal: AbortSignal.timeout(3000), redirect: 'manual'
|
||||
});
|
||||
if (response.ok || (response.status >= 300 && response.status < 400)) {
|
||||
ctx.log.info('docker', 'Health check passed', { containerId, status: response.status });
|
||||
@@ -290,7 +290,7 @@ module.exports = function(ctx) {
|
||||
await ctx.caddy.reload(existing);
|
||||
return;
|
||||
}
|
||||
const result = await ctx.caddy.modify(c => `${c }\n${config}\n`);
|
||||
const result = await ctx.caddy.modify(c => c + `\n${config}\n`);
|
||||
if (!result.success) throw new Error(`[DC-303] Failed to add Caddy config for ${domain}: ${result.error}`);
|
||||
await ctx.caddy.verifySite(domain);
|
||||
}
|
||||
@@ -405,6 +405,6 @@ module.exports = function(ctx) {
|
||||
removeSubpathConfig,
|
||||
ensureMainDomainBlock,
|
||||
RESERVED_SUBPATHS,
|
||||
generateStaticSiteConfig,
|
||||
generateStaticSiteConfig
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ module.exports = function(ctx, helpers) {
|
||||
try {
|
||||
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } });
|
||||
if (pruneResult.SpaceReclaimed > 0) {
|
||||
ctx.log.info('docker', 'Pruned dangling images after removal', { spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) }MB` });
|
||||
ctx.log.info('docker', 'Pruned dangling images after removal', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
||||
}
|
||||
} catch (pruneErr) {
|
||||
ctx.log.debug('docker', 'Image prune after removal failed', { error: pruneErr.message });
|
||||
@@ -42,7 +42,7 @@ module.exports = function(ctx, helpers) {
|
||||
try {
|
||||
const domain = ctx.buildDomain(subdomain);
|
||||
const getResult = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/get', {
|
||||
token: ctx.dns.getToken(), domain, zone: ctx.siteConfig.tld.replace(/^\./, ''), listZone: 'true',
|
||||
token: ctx.dns.getToken(), domain, zone: ctx.siteConfig.tld.replace(/^\./, ''), listZone: 'true'
|
||||
});
|
||||
let recordIp = ip || 'localhost';
|
||||
if (getResult.status === 'ok' && getResult.response?.records) {
|
||||
@@ -50,7 +50,7 @@ module.exports = function(ctx, helpers) {
|
||||
if (aRecord && aRecord.rData?.ipAddress) recordIp = aRecord.rData.ipAddress;
|
||||
}
|
||||
const dnsResult = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/delete', {
|
||||
token: ctx.dns.getToken(), domain, type: 'A', ipAddress: recordIp,
|
||||
token: ctx.dns.getToken(), domain, type: 'A', ipAddress: recordIp
|
||||
});
|
||||
results.dns = dnsResult.status === 'ok' ? 'deleted' : (dnsResult.errorMessage || 'failed');
|
||||
ctx.log.info('dns', 'DNS record removal', { result: results.dns });
|
||||
|
||||
@@ -37,7 +37,7 @@ module.exports = function(ctx, helpers) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'No services have deployment manifests to restore',
|
||||
results: [],
|
||||
results: []
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ module.exports = function(ctx, helpers) {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
status: 'failed',
|
||||
error: error.message,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ module.exports = function(ctx, helpers) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Restore complete: ${succeeded} restored, ${skipped} skipped, ${failed} failed`,
|
||||
results,
|
||||
results
|
||||
});
|
||||
}, 'apps-restore-all'));
|
||||
|
||||
@@ -81,7 +81,7 @@ module.exports = function(ctx, helpers) {
|
||||
hasManifest: !!service.deploymentManifest,
|
||||
templateId: service.deploymentManifest?.templateId || service.appTemplate || null,
|
||||
deployedAt: service.deployedAt || null,
|
||||
containerRunning: false,
|
||||
containerRunning: false
|
||||
};
|
||||
|
||||
// Check if container is currently running
|
||||
@@ -125,7 +125,7 @@ module.exports = function(ctx, helpers) {
|
||||
name: service.name,
|
||||
status: 'restored',
|
||||
type: 'static',
|
||||
message: `Static site "${service.name}" config preserved`,
|
||||
message: `Static site "${service.name}" config preserved`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ module.exports = function(ctx, helpers) {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
status: 'skipped',
|
||||
message: 'Container already running',
|
||||
message: 'Container already running'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -164,7 +164,7 @@ module.exports = function(ctx, helpers) {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
status: 'skipped',
|
||||
message: 'Container already running (found by name)',
|
||||
message: 'Container already running (found by name)'
|
||||
};
|
||||
}
|
||||
// Exists but not running — remove stale container
|
||||
@@ -178,7 +178,7 @@ module.exports = function(ctx, helpers) {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
status: 'failed',
|
||||
error: 'No container configuration in manifest',
|
||||
error: 'No container configuration in manifest'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ module.exports = function(ctx, helpers) {
|
||||
} catch (e) {
|
||||
// Check if image exists locally
|
||||
const images = await ctx.docker.client.listImages({
|
||||
filters: { reference: [manifest.container.image] },
|
||||
filters: { reference: [manifest.container.image] }
|
||||
});
|
||||
if (images.length === 0) {
|
||||
throw new Error(`Failed to pull image ${manifest.container.image}: ${e.message}`);
|
||||
@@ -206,7 +206,7 @@ module.exports = function(ctx, helpers) {
|
||||
PortBindings: {},
|
||||
Binds: manifest.container.volumes || [],
|
||||
RestartPolicy: { Name: 'unless-stopped' },
|
||||
LogConfig: DOCKER.LOG_CONFIG,
|
||||
LogConfig: DOCKER.LOG_CONFIG
|
||||
},
|
||||
Env: Object.entries(manifest.container.environment || {}).map(([k, v]) => `${k}=${v}`),
|
||||
Labels: {
|
||||
@@ -214,8 +214,8 @@ module.exports = function(ctx, helpers) {
|
||||
'sami.app': manifest.templateId,
|
||||
'sami.subdomain': manifest.config.subdomain,
|
||||
'sami.deployed': new Date().toISOString(),
|
||||
'sami.restored': 'true',
|
||||
},
|
||||
'sami.restored': 'true'
|
||||
}
|
||||
};
|
||||
|
||||
// Set up port bindings
|
||||
@@ -287,7 +287,7 @@ module.exports = function(ctx, helpers) {
|
||||
status: 'restored',
|
||||
type: 'container',
|
||||
containerId: container.id,
|
||||
message: `${service.name} restored successfully`,
|
||||
message: `${service.name} restored successfully`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = function(ctx, helpers) {
|
||||
success: true,
|
||||
templates: ctx.APP_TEMPLATES,
|
||||
categories: ctx.TEMPLATE_CATEGORIES,
|
||||
difficultyLevels: ctx.DIFFICULTY_LEVELS,
|
||||
difficultyLevels: ctx.DIFFICULTY_LEVELS
|
||||
});
|
||||
}, 'apps-templates'));
|
||||
|
||||
@@ -71,7 +71,7 @@ module.exports = function(ctx, helpers) {
|
||||
try {
|
||||
const oldDomain = oldSubdomain.includes('.') ? oldSubdomain : ctx.buildDomain(oldSubdomain);
|
||||
const result = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/delete', {
|
||||
token: ctx.dns.getToken(), domain: oldDomain, type: 'A', ipAddress: ip || 'localhost',
|
||||
token: ctx.dns.getToken(), domain: oldDomain, type: 'A', ipAddress: ip || 'localhost'
|
||||
});
|
||||
results.oldDns = result.status === 'ok' ? 'deleted' : result.errorMessage;
|
||||
ctx.log.info('dns', 'Old DNS record deleted', { domain: oldDomain });
|
||||
@@ -139,7 +139,7 @@ module.exports = function(ctx, helpers) {
|
||||
success: true,
|
||||
message: `Subdomain updated: ${oldSubdomain} -> ${newSubdomain}`,
|
||||
newUrl: `https://${ctx.buildDomain(newSubdomain)}`,
|
||||
results,
|
||||
results
|
||||
});
|
||||
}, 'update-subdomain'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user