fix(deploy): use Docker archive API to copy dist (no volume-source discovery)
This commit is contained in:
@@ -6,9 +6,11 @@
|
||||
// act_runner (v0.6.1) ignores both `container.options` extra --volume and
|
||||
// workflow `container.volumes` in this setup, so the host static dir can't be
|
||||
// bind-mounted directly into the job container. But the host docker.sock IS
|
||||
// mounted by default, so we launch a throwaway helper container that binds
|
||||
// BOTH the build workspace (a docker volume on the host) and /data/www/userbase,
|
||||
// and copies the built assets across.
|
||||
// mounted by default, so we use the Docker API to:
|
||||
// 1. tar the built assets straight out of THIS container's filesystem
|
||||
// (GET /containers/{jobId}/archive), and
|
||||
// 2. upload that tar into a throwaway helper container that has
|
||||
// /data/www/userbase mounted RW (PUT /containers/{helperId}/archive).
|
||||
//
|
||||
// No docker CLI and no network egress required — we speak the Docker HTTP API
|
||||
// over the unix socket directly with Node's http module.
|
||||
@@ -19,7 +21,7 @@ const fs = require('fs');
|
||||
const SOCKET = '/var/run/docker.sock';
|
||||
const HOST_STATIC = '/data/www/userbase';
|
||||
const HELPER_IMAGE = 'node:20'; // already cached on host (used by the job)
|
||||
const WS_MOUNT = '/workspace/admin/userbase';
|
||||
const WS_DIST = '/workspace/admin/userbase/front/dist/prod';
|
||||
|
||||
// The job container's ID, derived from its cgroup (robust regardless of hostname).
|
||||
function getContainerId() {
|
||||
@@ -29,7 +31,7 @@ function getContainerId() {
|
||||
return m[0];
|
||||
}
|
||||
|
||||
function dockerReq(method, path, bodyObj) {
|
||||
function dockerJson(method, path, bodyObj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = bodyObj ? JSON.stringify(bodyObj) : null;
|
||||
const headers = {};
|
||||
@@ -51,85 +53,81 @@ function dockerReq(method, path, bodyObj) {
|
||||
});
|
||||
}
|
||||
|
||||
// Locate the host path backing the workspace mount by inspecting THIS container
|
||||
// via the Docker daemon (docker.sock). This is far more reliable than parsing
|
||||
// /proc/self/mountinfo (which reports the underlying block device, not the bind
|
||||
// source). The job container's hostname is its short ID.
|
||||
async function findWorkspaceSource() {
|
||||
const id = getContainerId();
|
||||
const r = await dockerReq('GET', `/containers/${id}/json`);
|
||||
if (r.status >= 400) {
|
||||
throw new Error('inspect self failed: ' + r.status + ' ' + r.body);
|
||||
}
|
||||
const info = JSON.parse(r.body);
|
||||
const mounts = info.Mounts || [];
|
||||
for (const m of mounts) {
|
||||
if (m.Destination === WS_MOUNT) {
|
||||
return m.Source; // host path of the docker volume _data dir
|
||||
}
|
||||
}
|
||||
throw new Error('workspace mount ' + WS_MOUNT + ' not found in container inspect');
|
||||
// Returns the raw tar response stream for a container path.
|
||||
function getArchive(id, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{ socketPath: SOCKET, path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`, method: 'GET' },
|
||||
(res) => resolve(res)
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Pipes a tar stream into a container path via PUT /archive.
|
||||
function putArchive(id, path, tarStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
socketPath: SOCKET,
|
||||
path: `/containers/${id}/archive?path=${encodeURIComponent(path)}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/x-tar' },
|
||||
},
|
||||
(res) => {
|
||||
let buf = '';
|
||||
res.on('data', (c) => (buf += c));
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
||||
}
|
||||
);
|
||||
req.on('error', reject);
|
||||
tarStream.pipe(req);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const wsSource = await findWorkspaceSource();
|
||||
console.log('workspace source (host path):', wsSource);
|
||||
const jobId = getContainerId();
|
||||
console.log('job container id:', jobId);
|
||||
|
||||
// Inside the helper, the workspace is bound at /src (see Binds below).
|
||||
const SRC_IN_HELPER = '/src/front/dist/prod';
|
||||
// 1) tar of the built assets, fetched from THIS container's own filesystem.
|
||||
const tar = await getArchive(jobId, WS_DIST);
|
||||
if (tar.statusCode !== 200) {
|
||||
let msg = '';
|
||||
tar.on('data', (c) => (msg += c));
|
||||
await new Promise((r) => tar.on('end', r));
|
||||
throw new Error('get archive failed (status ' + tar.statusCode + '): ' + msg);
|
||||
}
|
||||
console.log('fetched dist tar (content-type:', tar.headers['content-type'], ')');
|
||||
|
||||
// 2) helper container with the host static dir mounted RW; keep it alive for the PUT.
|
||||
const createBody = {
|
||||
Image: HELPER_IMAGE,
|
||||
Tty: true,
|
||||
HostConfig: {
|
||||
NetworkMode: 'none',
|
||||
Binds: [`${wsSource}:/src:ro`, `${HOST_STATIC}:${HOST_STATIC}`],
|
||||
},
|
||||
Cmd: [
|
||||
'sh',
|
||||
'-c',
|
||||
// Write diagnostics to the host-mounted dir so we can inspect after the run.
|
||||
`DBG=${HOST_STATIC}/_debug.txt; ` +
|
||||
`echo "wsSource=${wsSource}" > $DBG; ` +
|
||||
`echo "--- ls /src/front/dist/prod ---" >> $DBG; ` +
|
||||
`ls -la ${SRC_IN_HELPER} >> $DBG 2>&1; ` +
|
||||
`echo "src file count: $(find ${SRC_IN_HELPER} -type f 2>/dev/null | wc -l)" >> $DBG; ` +
|
||||
`set -e; ` +
|
||||
`rm -rf ${HOST_STATIC}/front && mkdir -p ${HOST_STATIC}/front && ` +
|
||||
`cp -r ${SRC_IN_HELPER}/. ${HOST_STATIC}/front/ 2>> $DBG; ` +
|
||||
`echo "dest file count: $(find ${HOST_STATIC}/front -type f 2>/dev/null | wc -l)" >> $DBG; ` +
|
||||
`echo "copied"`,
|
||||
],
|
||||
HostConfig: { NetworkMode: 'none', Binds: [`${HOST_STATIC}:${HOST_STATIC}`] },
|
||||
Cmd: ['sh', '-c', `rm -rf ${HOST_STATIC}/front && mkdir -p ${HOST_STATIC}/front && exec tail -f /dev/null`],
|
||||
};
|
||||
|
||||
const created = await dockerReq('POST', '/containers/create', createBody);
|
||||
const created = await dockerJson('POST', '/containers/create', createBody);
|
||||
if (created.status >= 400) {
|
||||
throw new Error('create failed: ' + created.status + ' ' + created.body);
|
||||
}
|
||||
const id = JSON.parse(created.body).Id;
|
||||
console.log('helper container:', id);
|
||||
const helperId = JSON.parse(created.body).Id;
|
||||
console.log('helper container:', helperId);
|
||||
|
||||
const started = await dockerReq('POST', `/containers/${id}/start`);
|
||||
const started = await dockerJson('POST', `/containers/${helperId}/start`);
|
||||
if (started.status >= 400) {
|
||||
throw new Error('start failed: ' + started.status + ' ' + started.body);
|
||||
}
|
||||
|
||||
// Block until the helper exits.
|
||||
const waited = await dockerReq('POST', `/containers/${id}/wait`);
|
||||
const waitJson = JSON.parse(waited.body || '{}');
|
||||
console.log('helper exit code:', waitJson.StatusCode);
|
||||
|
||||
// Grab logs (Tty:true => raw, no multiplexing).
|
||||
const logs = await dockerReq('GET', `/containers/${id}/logs?stdout=1&stderr=1`);
|
||||
console.log('--- helper logs ---');
|
||||
console.log(logs.body);
|
||||
|
||||
// Cleanup.
|
||||
await dockerReq('DELETE', `/containers/${id}?force=true`);
|
||||
|
||||
if (waitJson.StatusCode !== 0) {
|
||||
throw new Error('helper exited with code ' + waitJson.StatusCode);
|
||||
// 3) upload the tar straight into the helper's mounted host dir.
|
||||
const put = await putArchive(helperId, `${HOST_STATIC}/front`, tar);
|
||||
console.log('put archive status:', put.status);
|
||||
if (put.status >= 400) {
|
||||
throw new Error('put archive failed: ' + put.status + ' ' + put.body);
|
||||
}
|
||||
|
||||
// 4) cleanup helper.
|
||||
await dockerJson('DELETE', `/containers/${helperId}?force=true`);
|
||||
|
||||
console.log('deploy OK ->', HOST_STATIC + '/front');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user