Files
pulse/server/configLoader.js
T
courtmanr@gmail.com 4b8fdf2156 fix: implement persistent Docker volume configuration to prevent data loss
Previously, configuration created via the web UI was lost when containers were
recreated or updated, requiring users to reconfigure each time. This was due to
the .env file being stored inside the container filesystem.

Changes:
- Add persistent volume mount for configuration directory in docker-compose.yml
- Update ConfigApi to auto-detect Docker vs development environment
- Configure dotenv loading to use persistent config path when available
- Update file watcher to monitor correct .env file location
- Remove conflicting host .env file mount
- Update README with new volume configuration instructions

Configuration now persists across container updates, eliminating the need for
users to reconfigure after updates.

Fixes #94

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-31 10:30:51 +01:00

271 lines
11 KiB
JavaScript

const { URL } = require('url');
// Align placeholder values with install script and .env.example
const placeholderValues = [
// Hostname parts - case-insensitive matching might be better if OS env vars differ.
// For now, direct case-sensitive include check.
'your-proxmox-ip-or-hostname',
'proxmox_host', // Substring for https://proxmox_host:8006 or similar
'YOUR_PBS_IP_OR_HOSTNAME', // For PBS host
// Token ID parts - these are more specific to example/guidance values
'user@pam!your-token-name', // Matches common PVE example format
'user@pbs!your-token-name', // Matches common PBS example format
'your-api-token-id', // Generic part often seen in examples
'user@pam!tokenid', // From original install script comment
'user@pbs!tokenid', // PBS variant of install script comment
// Secret parts
'your-token-secret-uuid', // Common PVE secret example
'your-pbs-token-secret-uuid', // Common PBS secret example
'YOUR_API_SECRET_HERE', // From original install script comment
'secret-uuid', // Specific value used in config.test.js
'your-api-token-uuid', // Specific value used in config.test.js for PROXMOX_TOKEN_SECRET
'your-port' // Specific value used in config.test.js for PROXMOX_PORT
];
// Error class for configuration issues
class ConfigurationError extends Error {
constructor(message) {
super(message);
this.name = 'ConfigurationError';
}
}
// Function to load PBS configuration
function loadPbsConfig(index = null) {
const suffix = index ? `_${index}` : '';
const hostVar = `PBS_HOST${suffix}`;
const tokenIdVar = `PBS_TOKEN_ID${suffix}`;
const tokenSecretVar = `PBS_TOKEN_SECRET${suffix}`;
const nodeNameVar = `PBS_NODE_NAME${suffix}`;
const portVar = `PBS_PORT${suffix}`;
const selfSignedVar = `PBS_ALLOW_SELF_SIGNED_CERTS${suffix}`;
const pbsHostUrl = process.env[hostVar];
if (!pbsHostUrl) {
return false; // No more PBS configs if PBS_HOST is missing
}
let pbsHostname = pbsHostUrl;
try {
const parsedUrl = new URL(pbsHostUrl);
pbsHostname = parsedUrl.hostname;
} catch (e) {
// console.warn(`WARN: Could not parse PBS_HOST URL "${pbsHostUrl}". Using full value as fallback name.`);
}
const pbsTokenId = process.env[tokenIdVar];
const pbsTokenSecret = process.env[tokenSecretVar];
let config = null;
let idPrefix = index ? `pbs_endpoint_${index}` : 'pbs_primary';
if (pbsTokenId && pbsTokenSecret) {
const pbsPlaceholders = placeholderValues.filter(p =>
pbsHostUrl.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p)
);
if (pbsPlaceholders.length > 0) {
console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (Token). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`);
} else {
config = {
id: `${idPrefix}_token`,
authMethod: 'token',
name: process.env[nodeNameVar] || pbsHostname,
host: pbsHostUrl,
port: process.env[portVar] || '8007',
tokenId: pbsTokenId,
tokenSecret: pbsTokenSecret,
nodeName: process.env[nodeNameVar], // Keep nodeName field
allowSelfSignedCerts: process.env[selfSignedVar] !== 'false',
enabled: true
};
console.log(`INFO: Found PBS configuration ${index || 'primary'} with ID: ${config.id}, name: ${config.name}, host: ${config.host}`);
}
} else {
console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`);
}
if (config) {
return { found: true, config: config }; // Return config if found
}
// Return found:true if host was set, but config was invalid/partial
return { found: !!pbsHostUrl, config: null };
}
// Main function to load all configurations
function loadConfiguration() {
// Only load .env file if not in test environment
if (process.env.NODE_ENV !== 'test') {
const fs = require('fs');
const path = require('path');
const configDir = path.join(__dirname, '../config');
const configEnvPath = path.join(configDir, '.env');
const projectEnvPath = path.join(__dirname, '../.env');
if (fs.existsSync(configEnvPath)) {
require('dotenv').config({ path: configEnvPath });
} else {
require('dotenv').config({ path: projectEnvPath });
}
}
let isConfigPlaceholder = false; // Add this flag
// --- Proxmox Primary Endpoint Validation ---
const primaryRequiredEnvVars = [
'PROXMOX_HOST',
'PROXMOX_TOKEN_ID',
'PROXMOX_TOKEN_SECRET'
];
let missingVars = [];
let placeholderVars = [];
primaryRequiredEnvVars.forEach(varName => {
const value = process.env[varName];
if (!value) {
missingVars.push(varName);
} else if (placeholderValues.some(placeholder => value.includes(placeholder) || placeholder.includes(value))) {
placeholderVars.push(varName);
}
});
// Throw error only if required vars are MISSING
if (missingVars.length > 0) {
console.warn('--- Configuration Warning ---');
console.warn(`Missing required environment variables: ${missingVars.join(', ')}.`);
console.warn('Pulse will start in setup mode. Please configure via the web interface.');
// Return minimal configuration to allow server to start
return {
endpoints: [],
pbsConfigs: [],
isConfigPlaceholder: true
};
}
// Set the flag if placeholders were detected (but don't throw error)
if (placeholderVars.length > 0) {
isConfigPlaceholder = true;
// Ensure token ID placeholder is included if missing
if (process.env.PROXMOX_TOKEN_ID && !placeholderVars.includes('PROXMOX_TOKEN_ID')) {
const hostIdx = placeholderVars.indexOf('PROXMOX_HOST');
if (hostIdx !== -1) placeholderVars.splice(hostIdx + 1, 0, 'PROXMOX_TOKEN_ID');
else placeholderVars.push('PROXMOX_TOKEN_ID');
}
console.warn(`WARN: Primary Proxmox environment variables seem to contain placeholder values: ${placeholderVars.join(', ')}. Pulse may not function correctly until configured.`);
}
// --- Load All Proxmox Endpoint Configurations ---
const endpoints = [];
function createProxmoxEndpointConfig(idPrefix, index, hostEnv, portEnv, tokenIdEnv, tokenSecretEnv, enabledEnv, selfSignedEnv, nodeNameEnv) {
const host = process.env[hostEnv];
const tokenId = process.env[tokenIdEnv];
const tokenSecret = process.env[tokenSecretEnv];
const nodeName = process.env[nodeNameEnv];
// Basic validation for additional endpoints (primary is validated earlier)
if (index !== null && (!tokenId || !tokenSecret)) {
// console.warn(`WARN: Skipping endpoint ${index || idPrefix} (Host: ${host}). Missing token ID or secret.`);
return null;
}
if (index !== null && placeholderValues.some(p => host.includes(p) || tokenId.includes(p) || tokenSecret.includes(p))) {
// console.warn(`WARN: Skipping endpoint ${index || idPrefix} (Host: ${host}). Environment variables seem to contain placeholder values.`);
return null;
}
return {
id: index ? `${idPrefix}_${index}` : idPrefix,
name: nodeName || host,
host: host,
port: process.env[portEnv] || '8006',
tokenId: tokenId,
tokenSecret: tokenSecret,
enabled: process.env[enabledEnv] !== 'false',
allowSelfSignedCerts: process.env[selfSignedEnv] !== 'false',
};
}
// Load primary endpoint (index null for helper)
const primaryEndpoint = createProxmoxEndpointConfig(
'primary',
null, // No index for primary
'PROXMOX_HOST',
'PROXMOX_PORT',
'PROXMOX_TOKEN_ID',
'PROXMOX_TOKEN_SECRET',
'PROXMOX_ENABLED',
'PROXMOX_ALLOW_SELF_SIGNED_CERTS',
'PROXMOX_NODE_NAME'
);
if (primaryEndpoint) { // Should always exist due to earlier checks, but good practice
endpoints.push(primaryEndpoint);
}
// Load additional Proxmox endpoints
let i = 2;
while (process.env[`PROXMOX_HOST_${i}`]) {
const additionalEndpoint = createProxmoxEndpointConfig(
'endpoint',
i,
`PROXMOX_HOST_${i}`,
`PROXMOX_PORT_${i}`,
`PROXMOX_TOKEN_ID_${i}`,
`PROXMOX_TOKEN_SECRET_${i}`,
`PROXMOX_ENABLED_${i}`,
`PROXMOX_ALLOW_SELF_SIGNED_CERTS_${i}`,
`PROXMOX_NODE_NAME_${i}`
);
if (additionalEndpoint) {
endpoints.push(additionalEndpoint);
}
i++;
}
if (endpoints.length > 1) {
// console.log(`INFO: Loaded configuration for ${endpoints.length} Proxmox endpoints.`);
}
// --- Load All PBS Configurations ---
const pbsConfigs = [];
// Load primary PBS config
const primaryPbsResult = loadPbsConfig();
/* istanbul ignore else */ // Ignore else path - tested by 'should not add primary PBS config if host is set but tokens are missing'
if (primaryPbsResult.config) {
pbsConfigs.push(primaryPbsResult.config);
}
// Load additional PBS configs
let pbsIndex = 2;
let pbsResult = loadPbsConfig(pbsIndex);
while (pbsResult.found) { // Continue as long as a PBS_HOST_n was found
if (pbsResult.config) {
pbsConfigs.push(pbsResult.config);
}
pbsIndex++;
pbsResult = loadPbsConfig(pbsIndex);
}
if (pbsConfigs.length > 0) {
// console.log(`INFO: Loaded configuration for ${pbsConfigs.length} PBS instances.`);
} else {
// console.log("INFO: No PBS instances configured.");
}
// --- Final Validation ---
const enabledEndpoints = endpoints.filter(e => e.enabled);
if (enabledEndpoints.length === 0 && pbsConfigs.length === 0) {
// Throw error instead of exiting
throw new ConfigurationError('\n--- Configuration Error ---\nNo enabled Proxmox VE or PBS endpoints could be configured. Please check your .env file and environment variables.\n');
}
// console.log('INFO: Configuration loaded successfully.');
// Return the flag along with endpoints and pbsConfigs
return { endpoints, pbsConfigs, isConfigPlaceholder };
}
module.exports = { loadConfiguration, ConfigurationError }; // Export the function and error class