136 lines
4.0 KiB
Bash
Executable File
136 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Generates a sanitized JSON snapshot of an OpenClaw container deployment.
|
|
# Safe-by-default: excludes environment values, secrets, mounts with secret-looking
|
|
# paths, and full labels/annotations. Intended for sharing with the agent.
|
|
#
|
|
# Usage:
|
|
# ./export-openclaw-host-facts.sh <container_name_or_id> [output.json]
|
|
# Example:
|
|
# ./export-openclaw-host-facts.sh openclaw ./openclaw-host-facts.json
|
|
|
|
CONTAINER="${1:-}"
|
|
OUT="${2:-./openclaw-host-facts.json}"
|
|
|
|
if [[ -z "$CONTAINER" ]]; then
|
|
echo "Usage: $0 <container_name_or_id> [output.json]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "docker not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TMP=$(mktemp)
|
|
trap 'rm -f "$TMP"' EXIT
|
|
|
|
docker inspect "$CONTAINER" > "$TMP"
|
|
|
|
node - <<'NODE' "$TMP" "$OUT"
|
|
const fs = require('fs');
|
|
const inFile = process.argv[2];
|
|
const outFile = process.argv[3];
|
|
const data = JSON.parse(fs.readFileSync(inFile, 'utf8'));
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
throw new Error('No inspect data found');
|
|
}
|
|
const c = data[0];
|
|
|
|
const secretish = /(secret|token|key|passwd|password|cookie|session|credential|auth|oauth|api[-_]?key|webhook)/i;
|
|
const sensitivePath = /(secret|secrets|private|\.ssh|gnupg|aws|gcloud|kube|\.env|credentials?)/i;
|
|
|
|
function uniq(arr) {
|
|
return [...new Set(arr.filter(Boolean))];
|
|
}
|
|
|
|
function safeMounts(mounts) {
|
|
return (mounts || [])
|
|
.filter(m => !(sensitivePath.test(m.Source || '') || sensitivePath.test(m.Destination || '')))
|
|
.map(m => ({
|
|
type: m.Type,
|
|
source: m.Source,
|
|
destination: m.Destination,
|
|
mode: m.Mode || '',
|
|
rw: !!m.RW,
|
|
}));
|
|
}
|
|
|
|
function safeEnv(env) {
|
|
const out = {};
|
|
for (const entry of env || []) {
|
|
const idx = entry.indexOf('=');
|
|
const key = idx === -1 ? entry : entry.slice(0, idx);
|
|
const value = idx === -1 ? '' : entry.slice(idx + 1);
|
|
if (secretish.test(key)) continue;
|
|
|
|
// Keep only clearly non-sensitive runtime facts.
|
|
if (/^(NODE_ENV|TZ|HOSTNAME|OPENCLAW_VARIANT|OPENCLAW_INSTALL_BROWSER|OPENCLAW_INSTALL_DOCKER_CLI)$/i.test(key)) {
|
|
out[key] = value;
|
|
} else {
|
|
out[key] = '<set>';
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const networkSettings = c.NetworkSettings || {};
|
|
const hostConfig = c.HostConfig || {};
|
|
const config = c.Config || {};
|
|
const ports = [];
|
|
for (const [containerPort, bindings] of Object.entries(networkSettings.Ports || {})) {
|
|
if (!bindings) {
|
|
ports.push({ container: containerPort, published: null });
|
|
continue;
|
|
}
|
|
for (const b of bindings) {
|
|
ports.push({
|
|
container: containerPort,
|
|
hostIp: b.HostIp,
|
|
hostPort: b.HostPort,
|
|
});
|
|
}
|
|
}
|
|
|
|
const summary = {
|
|
generatedAt: new Date().toISOString(),
|
|
source: 'docker inspect (sanitized)',
|
|
deploymentHint: 'komodo-or-docker',
|
|
container: {
|
|
name: (c.Name || '').replace(/^\//, ''),
|
|
idShort: (c.Id || '').slice(0, 12),
|
|
image: config.Image || null,
|
|
entrypoint: config.Entrypoint || null,
|
|
cmd: config.Cmd || null,
|
|
user: config.User || null,
|
|
workingDir: config.WorkingDir || null,
|
|
},
|
|
runtime: {
|
|
running: c.State?.Running || false,
|
|
status: c.State?.Status || null,
|
|
startedAt: c.State?.StartedAt || null,
|
|
restartPolicy: hostConfig.RestartPolicy?.Name || null,
|
|
privileged: !!hostConfig.Privileged,
|
|
networkMode: hostConfig.NetworkMode || null,
|
|
pidMode: hostConfig.PidMode || null,
|
|
ipcMode: hostConfig.IpcMode || null,
|
|
readOnlyRootfs: !!hostConfig.ReadonlyRootfs,
|
|
},
|
|
ports,
|
|
mounts: safeMounts(c.Mounts),
|
|
networks: Object.keys(networkSettings.Networks || {}),
|
|
aliases: uniq(Object.values(networkSettings.Networks || {}).flatMap(n => n.Aliases || [])),
|
|
env: safeEnv(config.Env),
|
|
labels: Object.fromEntries(
|
|
Object.entries(config.Labels || {}).filter(([k]) => {
|
|
const lk = String(k).toLowerCase();
|
|
return lk.startsWith('com.komodo.') || lk.startsWith('komodo.') || lk.startsWith('com.docker.compose.');
|
|
})
|
|
),
|
|
};
|
|
|
|
fs.writeFileSync(outFile, JSON.stringify(summary, null, 2));
|
|
console.log(`Wrote sanitized facts to ${outFile}`);
|
|
NODE
|