File size: 5,565 Bytes
919485b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env node

import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { isMainModule, pickFailureOutput, printJson, runCommand } from './command-center-utils.mjs';

const LEGACY_FIXTURE_CONTAINER = 'gipc-local-image-fixture';
const LEGACY_FIXTURE_DESTINATION = '/workspace';
const DOCKER_TIMEOUT_MS = 30_000;
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');

export function hasLegacyFixtureRepoMount(mounts, repoRoot = REPO_ROOT) {
    const resolvedRepoRoot = resolve(repoRoot);
    return mounts.some(
        (mount) =>
            mount?.Type === 'bind' &&
            typeof mount.Source === 'string' &&
            resolve(mount.Source) === resolvedRepoRoot &&
            mount.Destination === LEGACY_FIXTURE_DESTINATION
    );
}

export function parseDockerInspectContainer(output) {
    const parsed = JSON.parse(output);
    if (!Array.isArray(parsed) || !parsed[0] || typeof parsed[0] !== 'object') {
        throw new Error('docker inspect did not return a container object');
    }
    return parsed[0];
}

export function summarizeDockerMounts(mounts) {
    return mounts.map((mount) => ({
        type: mount.Type,
        source: mount.Source,
        destination: mount.Destination,
        mode: mount.Mode,
        writable: mount.RW
    }));
}

function parseArgs(argv) {
    const options = {
        dryRun: false,
        help: false,
        containerName: LEGACY_FIXTURE_CONTAINER
    };
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
        if (arg === '--dry-run') {
            options.dryRun = true;
            continue;
        }
        if (arg === '--help' || arg === '-h') {
            options.help = true;
            continue;
        }
        if (arg === '--container') {
            const value = argv[index + 1];
            if (!value) throw new Error('--container requires a value');
            options.containerName = value;
            index += 1;
            continue;
        }
        throw new Error(`Unknown option: ${arg}`);
    }
    return options;
}

function printHelp() {
    console.log(`用法:npm run docker:cleanup-fixtures -- [--dry-run] [--container <name>]

清理遗留的本地图片上游 Docker fixture 容器。该脚本只会删除同时满足以下条件的容器:

- 容器名匹配,默认 ${LEGACY_FIXTURE_CONTAINER}
- 存在 ${REPO_ROOT} -> ${LEGACY_FIXTURE_DESTINATION} 的整仓 bind mount

推荐使用 npm run smoke:image-upstream-local 启动本地 fixture。不要用 docker run -v "$PWD:${LEGACY_FIXTURE_DESTINATION}" 启动 fixture。`);
}

function isMissingContainer(result) {
    const output = `${result.stdout}\n${result.stderr}`;
    return /No such object|No such container/i.test(output);
}

export function buildAbsentReport(containerName) {
    return {
        ok: true,
        command: 'docker:cleanup-fixtures',
        container: containerName,
        present: false,
        unsafe_repo_mount: false,
        removed: false
    };
}

export function buildSkippedReport(containerName, mounts) {
    return {
        ok: true,
        command: 'docker:cleanup-fixtures',
        container: containerName,
        present: true,
        unsafe_repo_mount: false,
        removed: false,
        mounts: summarizeDockerMounts(mounts)
    };
}

function cleanupLegacyFixtureContainer(options) {
    const inspect = runCommand('docker', ['inspect', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS });
    if (!inspect.ok) {
        if (isMissingContainer(inspect)) return buildAbsentReport(options.containerName);
        return {
            ok: false,
            command: 'docker:cleanup-fixtures',
            phase: 'inspect',
            container: options.containerName,
            error: pickFailureOutput(inspect)
        };
    }

    let container;
    try {
        container = parseDockerInspectContainer(inspect.stdout);
    } catch (error) {
        return {
            ok: false,
            command: 'docker:cleanup-fixtures',
            phase: 'parse-inspect',
            container: options.containerName,
            error: error instanceof Error ? error.message : String(error)
        };
    }

    const mounts = Array.isArray(container.Mounts) ? container.Mounts : [];
    if (!hasLegacyFixtureRepoMount(mounts, REPO_ROOT)) return buildSkippedReport(options.containerName, mounts);
    if (options.dryRun) {
        return {
            ok: true,
            command: 'docker:cleanup-fixtures',
            container: options.containerName,
            present: true,
            unsafe_repo_mount: true,
            removed: false,
            dry_run: true,
            mounts: summarizeDockerMounts(mounts)
        };
    }

    const remove = runCommand('docker', ['rm', '-f', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS });
    return {
        ok: remove.ok,
        command: 'docker:cleanup-fixtures',
        container: options.containerName,
        present: true,
        unsafe_repo_mount: true,
        removed: remove.ok,
        mounts: summarizeDockerMounts(mounts),
        ...(remove.ok ? {} : { phase: 'remove', error: pickFailureOutput(remove) })
    };
}

function main() {
    const options = parseArgs(process.argv.slice(2));
    if (options.help) {
        printHelp();
        return;
    }
    const report = cleanupLegacyFixtureContainer(options);
    printJson(report);
    if (!report.ok) process.exitCode = 1;
}

if (isMainModule(import.meta.url, process.argv[1])) main();