[grade=B urn:ump:szxhoevzog44pvl3sz6n6qp2edv6wj6dcs3ajpi3kxbpbz7kikqa] DC-137: shipdeck engine branch for App Selector catalog installs
Opt-in (config.engine='shipdeck' + SHIPDECK_BRIDGE_URL configured + template compatible): catalog installs go through the bridge's validated image-install pipeline (digest-pinned OCI image, systemd release, Caddy gate, DNS, verify) instead of Docker. Port semantics: engine is host-networked, so the app LISTEN port (container side of the mapping, protocol suffix stripped) is gated — never the Docker host port; no mapping falls back to defaultPort. Volumes: absolute binds only, :ro preserved, named volumes and placeholders skipped. Engine installs skip panel DNS + Caddy (pipeline did them), record an engine=shipdeck manifest with the Shipdeckfile path, and removal runs shipdeck rm via the registry. Failures return 502 with stage detail and never fall back to Docker. Bridge unset = Docker path unchanged. 10 new tests (gating, payload mapping, route integration); full suite 138/138 suites 2933/2933 green. Judge: C -> C -> B zero-blockers.
This commit is contained in:
@@ -10,6 +10,7 @@ const { ValidationError } = require('../../src/utilities/errors');
|
||||
const { logError } = require('../../src/utils/logging');
|
||||
const { ok } = require('../../src/utils/responses');
|
||||
const { validateBody, schemas: valSchemas } = require('../../src/utilities/validate');
|
||||
const shipdeckEngine = require('../../src/apps-shipdeck-engine');
|
||||
/**
|
||||
* Apps deployment routes factory
|
||||
* @param {Object} deps - Explicit dependencies
|
||||
@@ -292,7 +293,29 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
// Process template variables for manifest (only needed for Docker containers)
|
||||
const processedTemplate = template.isStaticSite ? null : helpers.processTemplateVariables(template, config);
|
||||
|
||||
if (template.isStaticSite) {
|
||||
// DC-137: shipdeck engine branch — when the bridge is configured and
|
||||
// the template is engine-compatible, install via shipdeck (digest-
|
||||
// pinned image, systemd release, Caddy gate, DNS, verify) and skip
|
||||
// the Docker path entirely.
|
||||
let engineResult = null;
|
||||
if (!template.isStaticSite && !config.useExisting && config.engine === 'shipdeck' && shipdeckEngine.engineEnabledFor(template)) {
|
||||
try {
|
||||
engineResult = await shipdeckEngine.deployViaEngine({
|
||||
appId, template, config,
|
||||
processedTemplate: helpers.processTemplateVariables(template, config),
|
||||
log,
|
||||
});
|
||||
containerId = null;
|
||||
} catch (engineError) {
|
||||
// Engine failure is surfaced, never silently retried on Docker —
|
||||
// a fallback deploy would double-bind the subdomain and the
|
||||
// operator must see exactly which stage failed.
|
||||
await logError('app-deploy-engine', engineError, { appId, subdomain: config.subdomain });
|
||||
return errorResponse(res, 502, safeErrorMessage
|
||||
? safeErrorMessage(engineError.message)
|
||||
: `shipdeck engine install failed: ${engineError.message}`);
|
||||
}
|
||||
} else if (template.isStaticSite) {
|
||||
log.info('deploy', 'Deploying static site', { appId });
|
||||
if (appId === 'dashca') {
|
||||
await deployDashCAStaticSite(template, config);
|
||||
@@ -315,9 +338,12 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
|
||||
const isSubdirectoryMode = ctx.siteConfig.routingMode === 'subdirectory' && ctx.siteConfig.domain;
|
||||
|
||||
// DNS record creation (skip in subdirectory mode — only one domain needed)
|
||||
// DNS record creation (skip in subdirectory mode — only one domain needed;
|
||||
// also skipped for engine installs — shipdeck's pipeline already created it)
|
||||
let dnsWarning = null;
|
||||
if (config.createDns && !isSubdirectoryMode) {
|
||||
if (engineResult) {
|
||||
log.info('deploy', 'DNS handled by shipdeck engine', { appId, record: engineResult.service && engineResult.service.name });
|
||||
} else if (config.createDns && !isSubdirectoryMode) {
|
||||
try {
|
||||
await ctx.dns.universalCreateRecord(config.subdomain, config.ip);
|
||||
log.info('deploy', 'DNS record created', { domain: ctx.buildDomain(config.subdomain), ip: config.ip });
|
||||
@@ -335,7 +361,12 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
subpathSupport: template.subpathSupport || 'strip',
|
||||
};
|
||||
let caddyConfig;
|
||||
if (template.isStaticSite) {
|
||||
if (engineResult) {
|
||||
// Engine installs: shipdeck wrote the Caddy block already (tailnet-
|
||||
// only, gated). Nothing to generate or write here.
|
||||
caddyConfig = null;
|
||||
log.info('deploy', 'Caddy handled by shipdeck engine', { appId });
|
||||
} else if (template.isStaticSite) {
|
||||
const sitePath = platformPaths.sitePath(config.subdomain);
|
||||
if (appId === 'dashca') {
|
||||
caddyOptions.httpAccess = true;
|
||||
@@ -346,8 +377,11 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
caddyConfig = caddy.generateConfig(config.subdomain, config.ip, config.port || template.defaultPort, caddyOptions);
|
||||
}
|
||||
|
||||
// Write Caddy config (subdirectory: inject into main block; subdomain: append as new block)
|
||||
if (isSubdirectoryMode && !template.isStaticSite) {
|
||||
// Write Caddy config (subdirectory: inject into main block; subdomain:
|
||||
// append as new block; engine installs: already written by shipdeck)
|
||||
if (engineResult) {
|
||||
// no-op — pipeline wrote it
|
||||
} else if (isSubdirectoryMode && !template.isStaticSite) {
|
||||
await helpers.ensureMainDomainBlock();
|
||||
await helpers.addSubpathConfig(config.subdomain, caddyConfig);
|
||||
} else {
|
||||
@@ -358,9 +392,12 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
// Build service URL based on routing mode
|
||||
const serviceUrl = ctx.buildServiceUrl(config.subdomain);
|
||||
|
||||
// Build deployment manifest — the full recipe to recreate this container
|
||||
// Build deployment manifest — the full recipe to recreate this service.
|
||||
// Engine installs record the shipdeck recipe (digest-pinned Shipdeckfile
|
||||
// path) instead of a Docker container recipe.
|
||||
const deploymentManifest = {
|
||||
templateId: appId,
|
||||
engine: engineResult ? 'shipdeck' : 'docker',
|
||||
config: {
|
||||
subdomain: config.subdomain,
|
||||
port: config.port || template.defaultPort,
|
||||
@@ -372,7 +409,13 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
customVolumes: config.customVolumes || undefined,
|
||||
useExisting: false
|
||||
},
|
||||
container: template.isStaticSite ? null : {
|
||||
shipdeck: engineResult ? {
|
||||
service: engineResult.service && engineResult.service.name,
|
||||
image: engineResult.service && engineResult.service.image,
|
||||
shipdeckfile: engineResult.service && engineResult.service.shipdeckfile,
|
||||
port: engineResult.enginePort // actual engine listen port, not the Docker host port
|
||||
} : undefined,
|
||||
container: (!engineResult && !template.isStaticSite) ? {
|
||||
image: processedTemplate.docker.image,
|
||||
ports: processedTemplate.docker.ports,
|
||||
volumes: processedTemplate.docker.volumes || [],
|
||||
@@ -387,7 +430,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
return env;
|
||||
})(),
|
||||
capabilities: processedTemplate.docker.capabilities || undefined
|
||||
},
|
||||
} : null,
|
||||
caddy: {
|
||||
tailscaleOnly: config.tailscaleOnly || false,
|
||||
allowedIPs: config.allowedIPs || [],
|
||||
@@ -410,6 +453,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
|
||||
const response = {
|
||||
success: true, containerId, usedExisting,
|
||||
engine: engineResult ? 'shipdeck' : 'docker',
|
||||
url: serviceUrl,
|
||||
message: usedExisting ? `${template.name} configured using existing container!` : `${template.name} deployed successfully!`,
|
||||
setupInstructions: template.setupInstructions || []
|
||||
|
||||
Reference in New Issue
Block a user