Skip to content

Commit ab2c781

Browse files
authored
Refactor container cleanup into focused modules (#4217)
* Initial plan * refactor: split cleanup helpers into focused modules --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 7bc1bc2 commit ab2c781

10 files changed

Lines changed: 460 additions & 450 deletions

src/artifact-preservation.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as os from 'os';
4+
import execa from 'execa';
5+
import { logger } from './logger';
6+
7+
/**
8+
* Copies the iptables audit dump from the init-signal volume to the audit directory.
9+
* Must be called BEFORE stopContainers() because `docker compose down -v` destroys
10+
* the init-signal volume.
11+
*/
12+
export function preserveIptablesAudit(workDir: string, auditDir?: string): void {
13+
const iptablesAuditSrc = path.join(workDir, 'init-signal', 'iptables-audit.txt');
14+
const targetAuditDir = auditDir || path.join(workDir, 'audit');
15+
if (fs.existsSync(iptablesAuditSrc) && fs.existsSync(targetAuditDir)) {
16+
try {
17+
fs.copyFileSync(iptablesAuditSrc, path.join(targetAuditDir, 'iptables-audit.txt'));
18+
fs.chmodSync(path.join(targetAuditDir, 'iptables-audit.txt'), 0o644);
19+
logger.debug('Copied iptables audit state to audit directory');
20+
} catch (error) {
21+
logger.debug('Could not copy iptables audit file:', error);
22+
}
23+
}
24+
}
25+
26+
type PreserveDirectoryOptions = {
27+
runtimeDir?: string;
28+
runtimeSubdir?: string;
29+
workDir: string;
30+
workSubdir: string;
31+
destinationBaseName: string;
32+
timestamp: string;
33+
availableLabel: string;
34+
preservedLabel: string;
35+
permissionErrorMessage: string;
36+
preserveErrorMessage: string;
37+
chmodPreservedDir?: boolean;
38+
runtimeDirMustExist?: boolean;
39+
};
40+
41+
function preserveDirectory({
42+
runtimeDir,
43+
runtimeSubdir,
44+
workDir,
45+
workSubdir,
46+
destinationBaseName,
47+
timestamp,
48+
availableLabel,
49+
preservedLabel,
50+
permissionErrorMessage,
51+
preserveErrorMessage,
52+
chmodPreservedDir = false,
53+
runtimeDirMustExist = true,
54+
}: PreserveDirectoryOptions): void {
55+
if (runtimeDir) {
56+
const targetDir = runtimeSubdir ? path.join(runtimeDir, runtimeSubdir) : runtimeDir;
57+
if (!runtimeDirMustExist || fs.existsSync(targetDir)) {
58+
try {
59+
execa.sync('chmod', ['-R', 'a+rX', targetDir]);
60+
logger.info(`${availableLabel} available at: ${targetDir}`);
61+
} catch (error) {
62+
logger.debug(permissionErrorMessage, error);
63+
}
64+
}
65+
return;
66+
}
67+
68+
const sourceDir = path.join(workDir, workSubdir);
69+
const destinationDir = path.join(os.tmpdir(), `${destinationBaseName}-${timestamp}`);
70+
if (fs.existsSync(sourceDir) && fs.readdirSync(sourceDir).length > 0) {
71+
try {
72+
fs.renameSync(sourceDir, destinationDir);
73+
if (chmodPreservedDir) {
74+
execa.sync('chmod', ['-R', 'a+rX', destinationDir]);
75+
}
76+
logger.info(`${preservedLabel} preserved at: ${destinationDir}`);
77+
} catch (error) {
78+
logger.debug(preserveErrorMessage, error);
79+
}
80+
}
81+
}
82+
83+
type PreserveCleanupArtifactsOptions = {
84+
proxyLogsDir?: string;
85+
auditDir?: string;
86+
sessionStateDir?: string;
87+
};
88+
89+
export function preserveCleanupArtifacts(
90+
workDir: string,
91+
{ proxyLogsDir, auditDir, sessionStateDir }: PreserveCleanupArtifactsOptions = {},
92+
): void {
93+
const timestamp = path.basename(workDir).replace('awf-', '');
94+
const agentLogsDestination = path.join(os.tmpdir(), `awf-agent-logs-${timestamp}`);
95+
const agentLogsDir = path.join(workDir, 'agent-logs');
96+
if (fs.existsSync(agentLogsDir) && fs.readdirSync(agentLogsDir).length > 0) {
97+
try {
98+
fs.renameSync(agentLogsDir, agentLogsDestination);
99+
logger.info(`Agent logs preserved at: ${agentLogsDestination}`);
100+
} catch (error) {
101+
logger.debug('Could not preserve agent logs:', error);
102+
}
103+
}
104+
105+
preserveDirectory({
106+
runtimeDir: sessionStateDir,
107+
workDir,
108+
workSubdir: 'agent-session-state',
109+
destinationBaseName: 'awf-agent-session-state',
110+
timestamp,
111+
availableLabel: 'Agent session state',
112+
preservedLabel: 'Agent session state',
113+
permissionErrorMessage: 'Could not fix session state permissions:',
114+
preserveErrorMessage: 'Could not preserve agent session state:',
115+
});
116+
117+
preserveDirectory({
118+
runtimeDir: proxyLogsDir,
119+
runtimeSubdir: 'api-proxy-logs',
120+
workDir,
121+
workSubdir: 'api-proxy-logs',
122+
destinationBaseName: 'api-proxy-logs',
123+
timestamp,
124+
availableLabel: 'API proxy logs',
125+
preservedLabel: 'API proxy logs',
126+
permissionErrorMessage: 'Could not fix api-proxy log permissions:',
127+
preserveErrorMessage: 'Could not preserve api-proxy logs:',
128+
});
129+
130+
preserveDirectory({
131+
runtimeDir: proxyLogsDir,
132+
runtimeSubdir: 'cli-proxy-logs',
133+
workDir,
134+
workSubdir: 'cli-proxy-logs',
135+
destinationBaseName: 'cli-proxy-logs',
136+
timestamp,
137+
availableLabel: 'CLI proxy logs',
138+
preservedLabel: 'CLI proxy logs',
139+
permissionErrorMessage: 'Could not fix cli-proxy log permissions:',
140+
preserveErrorMessage: 'Could not preserve cli-proxy logs:',
141+
});
142+
143+
preserveDirectory({
144+
runtimeDir: proxyLogsDir,
145+
workDir,
146+
workSubdir: 'squid-logs',
147+
destinationBaseName: 'squid-logs',
148+
timestamp,
149+
availableLabel: 'Squid logs',
150+
preservedLabel: 'Squid logs',
151+
permissionErrorMessage: 'Could not fix squid log permissions:',
152+
preserveErrorMessage: 'Could not preserve squid logs:',
153+
chmodPreservedDir: true,
154+
runtimeDirMustExist: false,
155+
});
156+
157+
if (auditDir) {
158+
if (fs.existsSync(auditDir)) {
159+
try {
160+
execa.sync('chmod', ['-R', 'a+rX', auditDir]);
161+
logger.info(`Audit artifacts available at: ${auditDir}`);
162+
} catch (error) {
163+
logger.debug('Could not fix audit dir permissions:', error);
164+
}
165+
}
166+
} else {
167+
const defaultAuditDir = path.join(workDir, 'audit');
168+
const auditDestination = path.join(os.tmpdir(), `awf-audit-${timestamp}`);
169+
if (fs.existsSync(defaultAuditDir) && fs.readdirSync(defaultAuditDir).length > 0) {
170+
try {
171+
fs.renameSync(defaultAuditDir, auditDestination);
172+
execa.sync('chmod', ['-R', 'a+rX', auditDestination]);
173+
logger.info(`Audit artifacts preserved at: ${auditDestination}`);
174+
} catch (error) {
175+
logger.debug('Could not preserve audit artifacts:', error);
176+
}
177+
}
178+
}
179+
180+
const diagnosticsDir = path.join(workDir, 'diagnostics');
181+
if (fs.existsSync(diagnosticsDir) && fs.readdirSync(diagnosticsDir).length > 0) {
182+
if (auditDir) {
183+
const auditDiagnosticsDir = path.join(auditDir, 'diagnostics');
184+
try {
185+
fs.mkdirSync(auditDiagnosticsDir, { recursive: true });
186+
for (const file of fs.readdirSync(diagnosticsDir)) {
187+
fs.renameSync(path.join(diagnosticsDir, file), path.join(auditDiagnosticsDir, file));
188+
}
189+
execa.sync('chmod', ['-R', 'a+rX', auditDiagnosticsDir]);
190+
logger.info(`Diagnostic logs available at: ${auditDiagnosticsDir}`);
191+
} catch (error) {
192+
logger.debug('Could not move diagnostics to audit dir:', error);
193+
}
194+
} else {
195+
const diagnosticsDestination = path.join(os.tmpdir(), `awf-diagnostics-${timestamp}`);
196+
try {
197+
fs.mkdirSync(diagnosticsDestination, { recursive: true });
198+
for (const file of fs.readdirSync(diagnosticsDir)) {
199+
fs.renameSync(path.join(diagnosticsDir, file), path.join(diagnosticsDestination, file));
200+
}
201+
execa.sync('chmod', ['-R', 'a+rX', diagnosticsDestination]);
202+
logger.info(`Diagnostic logs preserved at: ${diagnosticsDestination}`);
203+
} catch (error) {
204+
logger.debug('Could not preserve diagnostic logs:', error);
205+
}
206+
}
207+
}
208+
}
209+
210+
export function removeWorkDirectories(workDir: string): void {
211+
fs.rmSync(workDir, { recursive: true, force: true });
212+
213+
const chrootHomeDir = `${workDir}-chroot-home`;
214+
if (fs.existsSync(chrootHomeDir)) {
215+
fs.rmSync(chrootHomeDir, { recursive: true, force: true });
216+
}
217+
}

src/compose-sanitizer.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as yaml from 'js-yaml';
2+
3+
function isSensitiveComposeEnvVar(name: string): boolean {
4+
return /(TOKEN|KEY|SECRET)/i.test(name);
5+
}
6+
7+
function sanitizeComposeEnvironment(environment: unknown): void {
8+
if (Array.isArray(environment)) {
9+
for (let i = 0; i < environment.length; i++) {
10+
const entry = environment[i];
11+
if (typeof entry !== 'string') {
12+
continue;
13+
}
14+
15+
const separatorIndex = entry.indexOf('=');
16+
if (separatorIndex === -1) {
17+
continue;
18+
}
19+
20+
const key = entry.slice(0, separatorIndex);
21+
if (isSensitiveComposeEnvVar(key)) {
22+
environment[i] = `${key}=[REDACTED]`;
23+
}
24+
}
25+
return;
26+
}
27+
28+
if (environment && typeof environment === 'object') {
29+
const values = environment as Record<string, unknown>;
30+
for (const key of Object.keys(values)) {
31+
if (isSensitiveComposeEnvVar(key)) {
32+
values[key] = '[REDACTED]';
33+
}
34+
}
35+
}
36+
}
37+
38+
export function sanitizeDockerComposeYaml(raw: string): string {
39+
const parsed = yaml.load(raw);
40+
if (!parsed || typeof parsed !== 'object') {
41+
return raw;
42+
}
43+
44+
const compose = parsed as Record<string, unknown>;
45+
const services = compose.services;
46+
if (!services || typeof services !== 'object' || Array.isArray(services)) {
47+
return yaml.dump(compose, { lineWidth: -1 });
48+
}
49+
50+
for (const service of Object.values(services as Record<string, unknown>)) {
51+
if (!service || typeof service !== 'object' || Array.isArray(service)) {
52+
continue;
53+
}
54+
55+
const serviceConfig = service as Record<string, unknown>;
56+
if ('environment' in serviceConfig) {
57+
sanitizeComposeEnvironment(serviceConfig.environment);
58+
}
59+
}
60+
61+
return yaml.dump(compose, { lineWidth: -1 });
62+
}

src/container-cleanup-branches.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
* - cleanup() branches for cli-proxy logs, audit dir, session state, and SSL
88
*/
99

10-
import { cleanup, collectDiagnosticLogs } from './container-cleanup';
10+
import { cleanup } from './container-cleanup';
11+
import { collectDiagnosticLogs } from './diagnostic-collector';
1112
import * as fs from 'fs';
1213
import * as path from 'path';
1314
import * as os from 'os';

0 commit comments

Comments
 (0)