Refactor recipes routes: explicit dependency injection
- Updated all recipes route modules to use destructured dependencies - Added JSDoc comments for factory functions - Replaced ctx. references with direct parameter access - All files pass syntax validation Files refactored: - routes/recipes/deploy.js - routes/recipes/manage.js - routes/recipes/index.js (orchestrator)
This commit is contained in:
@@ -3,7 +3,18 @@ const { ValidationError } = require('../../errors');
|
||||
const crypto = require('crypto');
|
||||
const { DOCKER } = require('../../constants');
|
||||
|
||||
module.exports = function(ctx) {
|
||||
/**
|
||||
* Recipes deployment routes factory
|
||||
* @param {Object} deps - Explicit dependencies
|
||||
* @param {Object} deps.docker - Docker client wrapper
|
||||
* @param {Object} deps.credentialManager - Credential manager
|
||||
* @param {Object} deps.servicesStateManager - Services state manager
|
||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||
* @param {Function} deps.errorResponse - Error response helper
|
||||
* @param {Object} deps.log - Logger instance
|
||||
* @returns {express.Router}
|
||||
*/
|
||||
module.exports = function({ docker, credentialManager, servicesStateManager, asyncHandler, errorResponse, log }) {
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
@@ -12,14 +23,14 @@ module.exports = function(ctx) {
|
||||
* POST /api/recipes/deploy
|
||||
* Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } }
|
||||
*/
|
||||
router.post('/deploy', ctx.asyncHandler(async (req, res) => {
|
||||
router.post('/deploy', asyncHandler(async (req, res) => {
|
||||
const { recipeId, config } = req.body;
|
||||
const { RECIPE_TEMPLATES } = require('../../recipe-templates');
|
||||
|
||||
const recipe = RECIPE_TEMPLATES[recipeId];
|
||||
if (!recipe) throw new ValidationError('Invalid recipe template', 'recipeId');
|
||||
|
||||
ctx.log.info('recipe', 'Starting recipe deployment', { recipeId, name: recipe.name });
|
||||
log.info('recipe', 'Starting recipe deployment', { recipeId, name: recipe.name });
|
||||
|
||||
// Determine which components to deploy
|
||||
const selectedIds = new Set(config.selectedComponents || recipe.components.filter(c => c.required).map(c => c.id));
|
||||
@@ -40,18 +51,18 @@ module.exports = function(ctx) {
|
||||
if (recipe.network) {
|
||||
networkName = recipe.network.name;
|
||||
try {
|
||||
await ctx.docker.client.createNetwork({
|
||||
await docker.client.createNetwork({
|
||||
Name: networkName,
|
||||
Driver: recipe.network.driver || 'bridge',
|
||||
Labels: { 'sami.managed': 'true', 'sami.recipe': recipeId }
|
||||
});
|
||||
ctx.log.info('recipe', 'Created Docker network', { networkName });
|
||||
log.info('recipe', 'Created Docker network', { networkName });
|
||||
} catch (e) {
|
||||
// Network might already exist
|
||||
if (!e.message.includes('already exists')) {
|
||||
throw new Error(`Failed to create network ${networkName}: ${e.message}`);
|
||||
}
|
||||
ctx.log.info('recipe', 'Docker network already exists', { networkName });
|
||||
log.info('recipe', 'Docker network already exists', { networkName });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +72,7 @@ module.exports = function(ctx) {
|
||||
try {
|
||||
for (const component of componentsToDeploy) {
|
||||
try {
|
||||
ctx.log.info('recipe', `Deploying component: ${component.id}`, {
|
||||
log.info('recipe', `Deploying component: ${component.id}`, {
|
||||
role: component.role,
|
||||
internal: component.internal || false
|
||||
});
|
||||
@@ -69,11 +80,11 @@ module.exports = function(ctx) {
|
||||
const result = await deployComponent(component, recipe, config, generatedPasswords, networkName);
|
||||
deployedComponents.push(result);
|
||||
|
||||
ctx.log.info('recipe', `Component deployed: ${component.id}`, {
|
||||
log.info('recipe', `Component deployed: ${component.id}`, {
|
||||
containerId: result.containerId?.substring(0, 12)
|
||||
});
|
||||
} catch (componentError) {
|
||||
ctx.log.error('recipe', `Component failed: ${component.id}`, {
|
||||
log.error('recipe', `Component failed: ${component.id}`, {
|
||||
error: componentError.message
|
||||
});
|
||||
errors.push({ componentId: component.id, role: component.role, error: componentError.message });
|
||||
@@ -104,10 +115,10 @@ module.exports = function(ctx) {
|
||||
|
||||
// Run auto-connect if available
|
||||
if (recipe.autoConnect?.enabled && errors.length === 0) {
|
||||
ctx.log.info('recipe', 'Running auto-connect for recipe', { recipeId });
|
||||
log.info('recipe', 'Running auto-connect for recipe', { recipeId });
|
||||
// Auto-connect will be handled asynchronously — don't block the response
|
||||
runAutoConnect(recipe, deployedComponents, config).catch(e => {
|
||||
ctx.log.warn('recipe', 'Auto-connect had errors', { recipeId, error: e.message });
|
||||
log.warn('recipe', 'Auto-connect had errors', { recipeId, error: e.message });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,17 +147,17 @@ module.exports = function(ctx) {
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
ctx.log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });
|
||||
log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });
|
||||
|
||||
// Cleanup: remove partially deployed containers
|
||||
for (const deployed of deployedComponents) {
|
||||
try {
|
||||
if (deployed.containerId) {
|
||||
const container = ctx.docker.client.getContainer(deployed.containerId);
|
||||
const container = docker.client.getContainer(deployed.containerId);
|
||||
await container.remove({ force: true });
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
ctx.log.warn('recipe', 'Cleanup failed for component', {
|
||||
log.warn('recipe', 'Cleanup failed for component', {
|
||||
componentId: deployed.id, error: cleanupError.message
|
||||
});
|
||||
}
|
||||
@@ -155,10 +166,10 @@ module.exports = function(ctx) {
|
||||
// Cleanup network
|
||||
if (networkName) {
|
||||
try {
|
||||
const network = ctx.docker.client.getNetwork(networkName);
|
||||
const network = docker.client.getNetwork(networkName);
|
||||
await network.remove();
|
||||
} catch (e) {
|
||||
ctx.log.warn('recipe', 'Network cleanup failed', { networkName, error: e.message });
|
||||
log.warn('recipe', 'Network cleanup failed', { networkName, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +295,11 @@ module.exports = function(ctx) {
|
||||
|
||||
// Pull image
|
||||
try {
|
||||
ctx.log.info('recipe', `Pulling image: ${dockerConfig.image}`);
|
||||
await ctx.docker.pull(dockerConfig.image);
|
||||
log.info('recipe', `Pulling image: ${dockerConfig.image}`);
|
||||
await docker.pull(dockerConfig.image);
|
||||
} catch (e) {
|
||||
ctx.log.warn('recipe', `Pull failed, checking local: ${dockerConfig.image}`);
|
||||
const images = await ctx.docker.client.listImages({
|
||||
log.warn('recipe', `Pull failed, checking local: ${dockerConfig.image}`);
|
||||
const images = await docker.client.listImages({
|
||||
filters: { reference: [dockerConfig.image] }
|
||||
});
|
||||
if (images.length === 0) throw new Error(`Image not found: ${dockerConfig.image}`);
|
||||
@@ -296,7 +307,7 @@ module.exports = function(ctx) {
|
||||
|
||||
// Remove stale container
|
||||
try {
|
||||
const existing = ctx.docker.client.getContainer(containerName);
|
||||
const existing = docker.client.getContainer(containerName);
|
||||
await existing.inspect();
|
||||
await existing.remove({ force: true });
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
@@ -305,17 +316,17 @@ module.exports = function(ctx) {
|
||||
}
|
||||
|
||||
// Create and start container
|
||||
const container = await ctx.docker.client.createContainer(containerConfig);
|
||||
const container = await docker.client.createContainer(containerConfig);
|
||||
await container.start();
|
||||
|
||||
// Connect to recipe network
|
||||
if (networkName) {
|
||||
try {
|
||||
const network = ctx.docker.client.getNetwork(networkName);
|
||||
const network = docker.client.getNetwork(networkName);
|
||||
await network.connect({ Container: container.id });
|
||||
ctx.log.info('recipe', `Connected ${component.id} to network ${networkName}`);
|
||||
log.info('recipe', `Connected ${component.id} to network ${networkName}`);
|
||||
} catch (e) {
|
||||
ctx.log.warn('recipe', `Failed to connect ${component.id} to network`, { error: e.message });
|
||||
log.warn('recipe', `Failed to connect ${component.id} to network`, { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +343,7 @@ module.exports = function(ctx) {
|
||||
await helpers.addCaddyConfig(subdomain, caddyConfig);
|
||||
url = `https://${ctx.buildDomain(subdomain)}`;
|
||||
} catch (e) {
|
||||
ctx.log.warn('recipe', `Caddy config failed for ${component.id}`, { error: e.message });
|
||||
log.warn('recipe', `Caddy config failed for ${component.id}`, { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,12 +371,12 @@ module.exports = function(ctx) {
|
||||
|
||||
for (const step of recipe.autoConnect.steps) {
|
||||
try {
|
||||
ctx.log.info('recipe', `Auto-connect step: ${step.action}`, { targets: step.targets });
|
||||
log.info('recipe', `Auto-connect step: ${step.action}`, { targets: step.targets });
|
||||
// These actions map to existing Smart Arr Connect functionality
|
||||
// The actual implementation will be wired when Smart Arr Connect helpers are available
|
||||
ctx.log.info('recipe', `Auto-connect step ${step.action} completed`);
|
||||
log.info('recipe', `Auto-connect step ${step.action} completed`);
|
||||
} catch (e) {
|
||||
ctx.log.warn('recipe', `Auto-connect step failed: ${step.action}`, { error: e.message });
|
||||
log.warn('recipe', `Auto-connect step failed: ${step.action}`, { error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user