diff --git a/dashcaddy-api/src/context/docker.js b/dashcaddy-api/src/context/docker.js new file mode 100644 index 0000000..ebbf39d --- /dev/null +++ b/dashcaddy-api/src/context/docker.js @@ -0,0 +1,78 @@ +/** + * Docker context + * Docker API wrapper and container utilities + */ + +const Docker = require('dockerode'); +const { DOCKER } = require('../../constants'); + +// Docker client instance +const docker = new Docker(); + +/** + * Pull a Docker image with timeout protection + * @param {string} imageName - Image name (e.g., 'nginx:latest') + * @param {number} timeoutMs - Timeout in milliseconds + * @returns {Promise} Pull progress output + */ +function dockerPull(imageName, timeoutMs = DOCKER.TIMEOUT) { + return new Promise((resolve, reject) => { + const timer = setTimeout( + () => reject(new Error(`Docker pull timed out after ${timeoutMs / 1000}s: ${imageName}`)), + timeoutMs, + ); + + docker.pull(imageName, (err, stream) => { + if (err) { + clearTimeout(timer); + return reject(err); + } + + docker.modem.followProgress(stream, (err, output) => { + clearTimeout(timer); + if (err) return reject(err); + resolve(output); + }); + }); + }); +} + +/** + * Find a running Docker container by name substring + * @param {string} name - Container name or substring + * @param {object} opts - Options (e.g., { all: true } to include stopped containers) + * @returns {Promise} Container object or null if not found + */ +async function findContainerByName(name, opts = { all: false }) { + const containers = await docker.listContainers(opts); + const match = containers.find((c) => + c.Names.some((n) => n.toLowerCase().includes(name.toLowerCase())), + ); + return match || null; +} + +/** + * Get all host ports currently in use by Docker containers + * @returns {Promise>} Set of port numbers + */ +async function getUsedPorts() { + const containers = await docker.listContainers({ all: false }); + const ports = new Set(); + + for (const c of containers) { + for (const p of (c.Ports || [])) { + if (p.PublicPort) { + ports.add(p.PublicPort); + } + } + } + + return ports; +} + +module.exports = { + client: docker, + pull: dockerPull, + findContainer: findContainerByName, + getUsedPorts, +}; diff --git a/dashcaddy-api/src/utils/index.js b/dashcaddy-api/src/utils/index.js index ba120bd..86ef7a5 100644 --- a/dashcaddy-api/src/utils/index.js +++ b/dashcaddy-api/src/utils/index.js @@ -6,10 +6,12 @@ const asyncHandler = require('./async-handler'); const { errorResponse, ok } = require('./responses'); const { safeErrorMessage } = require('./safe-error'); +const log = require('./logger'); module.exports = { asyncHandler, errorResponse, ok, safeErrorMessage, + log, }; diff --git a/dashcaddy-api/src/utils/logger.js b/dashcaddy-api/src/utils/logger.js new file mode 100644 index 0000000..bcfcb0d --- /dev/null +++ b/dashcaddy-api/src/utils/logger.js @@ -0,0 +1,40 @@ +/** + * Structured logging + * JSON-formatted logs with levels (debug, info, warn, error) + */ + +const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; +const LOG_LEVEL = LOG_LEVELS[process.env.LOG_LEVEL || 'info'] || 1; + +/** + * Core log function + * @param {string} level - Log level (debug, info, warn, error) + * @param {string} context - Context label (e.g., 'server', 'docker', 'caddy') + * @param {string} message - Log message + * @param {object} data - Additional structured data + */ +function log(level, context, message, data = {}) { + if (LOG_LEVELS[level] < LOG_LEVEL) return; + + const entry = { + t: new Date().toISOString(), + level, + ctx: context, + msg: message, + }; + + if (Object.keys(data).length) { + entry.data = data; + } + + const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log; + fn(JSON.stringify(entry)); +} + +// Convenience methods +log.info = (ctx, msg, data) => log('info', ctx, msg, data); +log.warn = (ctx, msg, data) => log('warn', ctx, msg, data); +log.error = (ctx, msg, data) => log('error', ctx, msg, data); +log.debug = (ctx, msg, data) => log('debug', ctx, msg, data); + +module.exports = log;