Atomically write files to avoid races with traefik

This commit is contained in:
Owen
2026-09-15 09:49:45 -04:00
parent 25b151a021
commit 143b4cf757
+47 -23
View File
@@ -717,10 +717,9 @@ export class TraefikConfigManager {
} }
if (shouldWrite) { if (shouldWrite) {
try { try {
fs.writeFileSync( this.atomicWriteFileSync(
traefikDynamicConfigPath, traefikDynamicConfigPath,
yaml.dump(traefikConfig, { noRefs: true }), yaml.dump(traefikConfig, { noRefs: true })
"utf8"
); );
logger.info("Traefik dynamic config updated"); logger.info("Traefik dynamic config updated");
} catch (err) { } catch (err) {
@@ -822,7 +821,7 @@ export class TraefikConfigManager {
// Only write the config if it has changed // Only write the config if it has changed
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
if (newConfigYaml !== originalConfigYaml) { if (newConfigYaml !== originalConfigYaml) {
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); this.atomicWriteFileSync(dynamicConfigPath, newConfigYaml);
logger.info("Dynamic cert config updated from local certificates"); logger.info("Dynamic cert config updated from local certificates");
} }
} }
@@ -900,26 +899,23 @@ export class TraefikConfigManager {
`Processing certificate for domain: ${cert.domain}` `Processing certificate for domain: ${cert.domain}`
); );
fs.writeFileSync(certPath, cert.certFile, "utf8"); // Write atomically (temp file + rename) so Traefik's
fs.writeFileSync(keyPath, cert.keyFile, "utf8"); // file watcher never observes a partially written
// cert/key and fails with "failed to find any PEM data".
// Set appropriate permissions (readable by owner only for key file) this.atomicWriteFileSync(certPath, cert.certFile, 0o644);
fs.chmodSync(certPath, 0o644); this.atomicWriteFileSync(keyPath, cert.keyFile, 0o600);
fs.chmodSync(keyPath, 0o600);
// Write/update .last_update file with current timestamp // Write/update .last_update file with current timestamp
fs.writeFileSync( this.atomicWriteFileSync(
lastUpdatePath, lastUpdatePath,
new Date().toISOString(), new Date().toISOString()
"utf8"
); );
// Check if this is a wildcard certificate and store it // Check if this is a wildcard certificate and store it
const wildcardPath = path.join(domainDir, ".wildcard"); const wildcardPath = path.join(domainDir, ".wildcard");
fs.writeFileSync( this.atomicWriteFileSync(
wildcardPath, wildcardPath,
cert.wildcard ? "true" : "false", cert.wildcard ? "true" : "false"
"utf8"
); );
logger.info( logger.info(
@@ -931,10 +927,9 @@ export class TraefikConfigManager {
// even if the cert content didn't change // even if the cert content didn't change
if (cert.expiresAt) { if (cert.expiresAt) {
const expiresAtPath = path.join(domainDir, ".expires_at"); const expiresAtPath = path.join(domainDir, ".expires_at");
fs.writeFileSync( this.atomicWriteFileSync(
expiresAtPath, expiresAtPath,
cert.expiresAt.toString(), cert.expiresAt.toString()
"utf8"
); );
} }
@@ -970,7 +965,7 @@ export class TraefikConfigManager {
// Only write the config if it has changed // Only write the config if it has changed
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true }); const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
if (newConfigYaml !== originalConfigYaml) { if (newConfigYaml !== originalConfigYaml) {
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8"); this.atomicWriteFileSync(dynamicConfigPath, newConfigYaml);
logger.info("Dynamic cert config updated"); logger.info("Dynamic cert config updated");
} }
} }
@@ -1141,10 +1136,9 @@ export class TraefikConfigManager {
if (configChanged) { if (configChanged) {
try { try {
fs.writeFileSync( this.atomicWriteFileSync(
dynamicConfigPath, dynamicConfigPath,
yaml.dump(dynamicConfig, { noRefs: true }), yaml.dump(dynamicConfig, { noRefs: true })
"utf8"
); );
logger.info("Dynamic config updated after cleanup"); logger.info("Dynamic config updated after cleanup");
} catch (err) { } catch (err) {
@@ -1171,6 +1165,36 @@ export class TraefikConfigManager {
} }
} }
/**
* Write a file atomically by writing to a temp file in the same
* directory and renaming it into place. This avoids Traefik (which
* watches these files/directories) picking up a partially written
* file and failing to parse it (e.g. "failed to find any PEM data").
*/
private atomicWriteFileSync(
filePath: string,
data: string,
mode?: number
): void {
const dir = path.dirname(filePath);
const tmpPath = path.join(
dir,
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
try {
fs.writeFileSync(tmpPath, data, "utf8");
if (mode !== undefined) {
fs.chmodSync(tmpPath, mode);
}
fs.renameSync(tmpPath, filePath);
} catch (error) {
try {
fs.rmSync(tmpPath, { force: true });
} catch {}
throw error;
}
}
/** /**
* Check if file exists * Check if file exists
*/ */