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:
Krystie
2026-03-29 21:45:28 -07:00
parent 77ae8171b8
commit 3de65dbf81
3 changed files with 83 additions and 50 deletions

View File

@@ -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 });
}
}
}