Make more efficient the cert get

This commit is contained in:
Owen 2025-08-17 20:18:10 -07:00
parent 33a2ac402c
commit 8c8a981452
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD

View file

@ -12,6 +12,13 @@ export class TraefikConfigManager {
private isRunning = false; private isRunning = false;
private activeDomains = new Set<string>(); private activeDomains = new Set<string>();
private timeoutId: NodeJS.Timeout | null = null; private timeoutId: NodeJS.Timeout | null = null;
private lastCertificateFetch: Date | null = null;
private lastKnownDomains = new Set<string>();
private lastLocalCertificateState = new Map<string, {
exists: boolean;
lastModified: Date | null;
expiresAt: Date | null;
}>();
constructor() {} constructor() {}
@ -50,6 +57,10 @@ export class TraefikConfigManager {
config.getRawConfig().traefik.certificates_path config.getRawConfig().traefik.certificates_path
); );
// Initialize local certificate state
this.lastLocalCertificateState = await this.scanLocalCertificateState();
logger.info(`Found ${this.lastLocalCertificateState.size} existing certificate directories`);
// Run initial check // Run initial check
await this.HandleTraefikConfig(); await this.HandleTraefikConfig();
@ -80,6 +91,113 @@ export class TraefikConfigManager {
logger.info("Certificate monitor stopped"); logger.info("Certificate monitor stopped");
} }
/**
* Scan local certificate directories to build current state
*/
private async scanLocalCertificateState(): Promise<Map<string, {
exists: boolean;
lastModified: Date | null;
expiresAt: Date | null;
}>> {
const state = new Map();
const certsPath = config.getRawConfig().traefik.certificates_path;
try {
if (!fs.existsSync(certsPath)) {
return state;
}
const certDirs = fs.readdirSync(certsPath, { withFileTypes: true });
for (const dirent of certDirs) {
if (!dirent.isDirectory()) continue;
const domain = dirent.name;
const domainDir = path.join(certsPath, domain);
const certPath = path.join(domainDir, "cert.pem");
const keyPath = path.join(domainDir, "key.pem");
const lastUpdatePath = path.join(domainDir, ".last_update");
const certExists = await this.fileExists(certPath);
const keyExists = await this.fileExists(keyPath);
const lastUpdateExists = await this.fileExists(lastUpdatePath);
let lastModified: Date | null = null;
let expiresAt: Date | null = null;
if (lastUpdateExists) {
try {
const lastUpdateStr = fs.readFileSync(lastUpdatePath, "utf8").trim();
lastModified = new Date(lastUpdateStr);
} catch {
// If we can't read the last update, fall back to file stats
try {
const stats = fs.statSync(certPath);
lastModified = stats.mtime;
} catch {
lastModified = null;
}
}
}
state.set(domain, {
exists: certExists && keyExists,
lastModified,
expiresAt
});
}
} catch (error) {
logger.error("Error scanning local certificate state:", error);
}
return state;
}
/**
* Check if we need to fetch certificates from remote
*/
private shouldFetchCertificates(currentDomains: Set<string>): boolean {
// Always fetch on first run
if (!this.lastCertificateFetch) {
return true;
}
// Fetch if it's been more than 24 hours (for renewals)
const dayInMs = 24 * 60 * 60 * 1000;
const timeSinceLastFetch = Date.now() - this.lastCertificateFetch.getTime();
if (timeSinceLastFetch > dayInMs) {
logger.info("Fetching certificates due to 24-hour renewal check");
return true;
}
// Fetch if domains have changed
if (this.lastKnownDomains.size !== currentDomains.size ||
!Array.from(this.lastKnownDomains).every(domain => currentDomains.has(domain))) {
logger.info("Fetching certificates due to domain changes");
return true;
}
// Check if any local certificates are missing or appear to be outdated
for (const domain of currentDomains) {
const localState = this.lastLocalCertificateState.get(domain);
if (!localState || !localState.exists) {
logger.info(`Fetching certificates due to missing local cert for ${domain}`);
return true;
}
// Check if certificate is expiring soon (within 30 days)
if (localState.expiresAt) {
const daysUntilExpiry = (localState.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry < 30) {
logger.info(`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`);
return true;
}
}
}
return false;
}
/** /**
* Main monitoring logic * Main monitoring logic
*/ */
@ -115,14 +233,37 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains); this.lastActiveDomains = new Set(domains);
} }
// Get valid certificates for active domains // Scan current local certificate state
const validCertificates = this.lastLocalCertificateState = await this.scanLocalCertificateState();
await this.getValidCertificatesForDomains(domains);
// logger.debug(`Valid certs array: ${JSON.stringify(validCertificates)}`); // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
let validCertificates: Array<{
id: number;
domain: string;
certFile: string | null;
keyFile: string | null;
expiresAt: Date | null;
updatedAt?: Date | null;
}> = [];
// Download and decrypt new certificates if (this.shouldFetchCertificates(domains)) {
await this.processValidCertificates(validCertificates); // Get valid certificates for active domains
validCertificates = await this.getValidCertificatesForDomains(domains);
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info(`Fetched ${validCertificates.length} certificates from remote`);
// Download and decrypt new certificates
await this.processValidCertificates(validCertificates);
} else {
const timeSinceLastFetch = this.lastCertificateFetch ?
Math.round((Date.now() - this.lastCertificateFetch.getTime()) / (1000 * 60)) : 0;
logger.debug(`Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`);
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
}
// Clean up certificates for domains no longer in use // Clean up certificates for domains no longer in use
await this.cleanupUnusedCertificates(domains); await this.cleanupUnusedCertificates(domains);
@ -301,6 +442,59 @@ export class TraefikConfigManager {
} }
} }
/**
* Update dynamic config from existing local certificates without fetching from remote
*/
private async updateDynamicConfigFromLocalCerts(domains: Set<string>): Promise<void> {
const dynamicConfigPath = config.getRawConfig().traefik.dynamic_cert_config_path;
// Load existing dynamic config if it exists, otherwise initialize
let dynamicConfig: any = { tls: { certificates: [] } };
if (fs.existsSync(dynamicConfigPath)) {
try {
const fileContent = fs.readFileSync(dynamicConfigPath, "utf8");
dynamicConfig = yaml.load(fileContent) || dynamicConfig;
if (!dynamicConfig.tls) dynamicConfig.tls = { certificates: [] };
if (!Array.isArray(dynamicConfig.tls.certificates)) {
dynamicConfig.tls.certificates = [];
}
} catch (err) {
logger.error("Failed to load existing dynamic config:", err);
}
}
// Keep a copy of the original config for comparison
const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
// Clear existing certificates and rebuild from local state
dynamicConfig.tls.certificates = [];
for (const domain of domains) {
const localState = this.lastLocalCertificateState.get(domain);
if (localState && localState.exists) {
const domainDir = path.join(
config.getRawConfig().traefik.certificates_path,
domain
);
const certPath = path.join(domainDir, "cert.pem");
const keyPath = path.join(domainDir, "key.pem");
const certEntry = {
certFile: `/var/${certPath}`,
keyFile: `/var/${keyPath}`
};
dynamicConfig.tls.certificates.push(certEntry);
}
}
// Only write the config if it has changed
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
if (newConfigYaml !== originalConfigYaml) {
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8");
logger.info("Dynamic cert config updated from local certificates");
}
}
/** /**
* Get valid certificates for the specified domains * Get valid certificates for the specified domains
*/ */
@ -446,6 +640,13 @@ export class TraefikConfigManager {
logger.info( logger.info(
`Certificate updated for domain: ${cert.domain}` `Certificate updated for domain: ${cert.domain}`
); );
// Update local state tracking
this.lastLocalCertificateState.set(cert.domain, {
exists: true,
lastModified: new Date(),
expiresAt: cert.expiresAt
});
} }
// Always ensure the config entry exists and is up to date // Always ensure the config entry exists and is up to date
@ -591,6 +792,9 @@ export class TraefikConfigManager {
); );
fs.rmSync(domainDir, { recursive: true, force: true }); fs.rmSync(domainDir, { recursive: true, force: true });
// Remove from local state tracking
this.lastLocalCertificateState.delete(dirName);
// Remove from dynamic config // Remove from dynamic config
const certFilePath = `/var/${path.join( const certFilePath = `/var/${path.join(
domainDir, domainDir,
@ -657,6 +861,16 @@ export class TraefikConfigManager {
} }
} }
/**
* Force a certificate refresh regardless of cache state
*/
public async forceCertificateRefresh(): Promise<void> {
logger.info("Forcing certificate refresh");
this.lastCertificateFetch = null;
this.lastKnownDomains = new Set();
await this.HandleTraefikConfig();
}
/** /**
* Get current status * Get current status
*/ */
@ -664,12 +878,16 @@ export class TraefikConfigManager {
isRunning: boolean; isRunning: boolean;
activeDomains: string[]; activeDomains: string[];
monitorInterval: number; monitorInterval: number;
lastCertificateFetch: Date | null;
localCertificateCount: number;
} { } {
return { return {
isRunning: this.isRunning, isRunning: this.isRunning,
activeDomains: Array.from(this.activeDomains), activeDomains: Array.from(this.activeDomains),
monitorInterval: monitorInterval:
config.getRawConfig().traefik.monitor_interval || 5000 config.getRawConfig().traefik.monitor_interval || 5000,
lastCertificateFetch: this.lastCertificateFetch,
localCertificateCount: this.lastLocalCertificateState.size
}; };
} }
} }