Initial commit: OpenClaw ops workspace
This commit is contained in:
244
scripts/export-openclaw-runtime-facts.sh
Executable file
244
scripts/export-openclaw-runtime-facts.sh
Executable file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# v2: Export sanitized OpenClaw runtime facts from Docker + optional Komodo API.
|
||||
# Safe by default:
|
||||
# - no secret env var values
|
||||
# - no obvious secret-ish mount paths
|
||||
# - no raw Komodo payload dump
|
||||
# - only selected/sanitized metadata is emitted
|
||||
#
|
||||
# Usage:
|
||||
# ./export-openclaw-runtime-facts.sh <container_name_or_id> [output.json]
|
||||
#
|
||||
# Optional env:
|
||||
# KOMODO_URL=https://komodo.example/api
|
||||
# KOMODO_TOKEN=read_only_token
|
||||
# KOMODO_STACK_NAME=openclaw
|
||||
# KOMODO_RESOURCE_ID=optional-id
|
||||
# KOMODO_VERIFY_TLS=true|false (default: true)
|
||||
#
|
||||
# Example:
|
||||
# KOMODO_URL=https://komodo.example/api \
|
||||
# KOMODO_TOKEN=... \
|
||||
# KOMODO_STACK_NAME=openclaw \
|
||||
# ./export-openclaw-runtime-facts.sh openclaw ./openclaw-runtime-facts.json
|
||||
|
||||
CONTAINER="${1:-}"
|
||||
OUT="${2:-./openclaw-runtime-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
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "node not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOCKER_JSON=$(mktemp)
|
||||
KOMODO_JSON=$(mktemp)
|
||||
trap 'rm -f "$DOCKER_JSON" "$KOMODO_JSON"' EXIT
|
||||
|
||||
docker inspect "$CONTAINER" > "$DOCKER_JSON"
|
||||
|
||||
KOMODO_STATUS="not_configured"
|
||||
if [[ -n "${KOMODO_URL:-}" && -n "${KOMODO_TOKEN:-}" ]]; then
|
||||
CURL_OPTS=(-sS -H "Authorization: Bearer ${KOMODO_TOKEN}" -H "Accept: application/json")
|
||||
if [[ "${KOMODO_VERIFY_TLS:-true}" == "false" ]]; then
|
||||
CURL_OPTS+=(-k)
|
||||
fi
|
||||
|
||||
# Try a few likely read-only endpoints/patterns without assuming one exact API shape.
|
||||
# The Node merger below is tolerant of missing/unexpected payloads.
|
||||
{
|
||||
echo '{"attempts":['
|
||||
first=1
|
||||
for path in \
|
||||
"/api/stacks" \
|
||||
"/api/stack" \
|
||||
"/api/deployments" \
|
||||
"/api/deployment" \
|
||||
"/api/resources"; do
|
||||
if resp=$(curl "${CURL_OPTS[@]}" "${KOMODO_URL%/}${path}" 2>/dev/null); then
|
||||
[[ $first -eq 0 ]] && echo ','
|
||||
first=0
|
||||
printf '{"path":%s,"body":%s}' "$(node -p 'JSON.stringify(process.argv[1])' "$path")" "$(printf '%s' "$resp" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.stringify(s)))')"
|
||||
fi
|
||||
done
|
||||
echo ']}'
|
||||
} > "$KOMODO_JSON" || true
|
||||
|
||||
if [[ -s "$KOMODO_JSON" ]]; then
|
||||
KOMODO_STATUS="queried"
|
||||
else
|
||||
KOMODO_STATUS="query_failed"
|
||||
fi
|
||||
else
|
||||
echo '{"attempts":[]}' > "$KOMODO_JSON"
|
||||
fi
|
||||
|
||||
node - <<'NODE' "$DOCKER_JSON" "$KOMODO_JSON" "$OUT" "$KOMODO_STATUS" "${KOMODO_STACK_NAME:-}" "${KOMODO_RESOURCE_ID:-}"
|
||||
const fs = require('fs');
|
||||
|
||||
const dockerFile = process.argv[2];
|
||||
const komodoFile = process.argv[3];
|
||||
const outFile = process.argv[4];
|
||||
const komodoStatus = process.argv[5];
|
||||
const hintedStackName = process.argv[6] || null;
|
||||
const hintedResourceId = process.argv[7] || null;
|
||||
|
||||
const dockerData = JSON.parse(fs.readFileSync(dockerFile, 'utf8'));
|
||||
if (!Array.isArray(dockerData) || dockerData.length === 0) throw new Error('No docker inspect data');
|
||||
const c = dockerData[0];
|
||||
const komodoRaw = JSON.parse(fs.readFileSync(komodoFile, 'utf8'));
|
||||
|
||||
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 short(v, n=12) { return typeof v === 'string' ? v.slice(0, n) : v; }
|
||||
function isObj(v) { return v && typeof v === 'object' && !Array.isArray(v); }
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
function safeLabels(labels) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(labels || {}).filter(([k, v]) => {
|
||||
const lk = String(k).toLowerCase();
|
||||
if (secretish.test(lk)) return false;
|
||||
return lk.startsWith('com.komodo.') || lk.startsWith('komodo.') || lk.startsWith('com.docker.compose.');
|
||||
}).map(([k, v]) => [k, String(v)])
|
||||
);
|
||||
}
|
||||
|
||||
function parseIfJson(s) {
|
||||
try { return JSON.parse(s); } catch { return s; }
|
||||
}
|
||||
|
||||
function walkForCandidates(node, acc = []) {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) walkForCandidates(item, acc);
|
||||
return acc;
|
||||
}
|
||||
if (!isObj(node)) return acc;
|
||||
|
||||
const lowerKeys = Object.keys(node).map(k => k.toLowerCase());
|
||||
const joined = lowerKeys.join(' ');
|
||||
if (/(stack|deployment|service|container|compose|docker)/.test(joined)) acc.push(node);
|
||||
|
||||
for (const v of Object.values(node)) walkForCandidates(v, acc);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function summarizeKomodo(raw) {
|
||||
const attempts = raw.attempts || [];
|
||||
const parsedBodies = attempts.map(a => ({ path: a.path, body: parseIfJson(a.body) }));
|
||||
const candidates = parsedBodies.flatMap(a => walkForCandidates(a.body, []));
|
||||
|
||||
const textBlob = JSON.stringify(parsedBodies).toLowerCase();
|
||||
|
||||
const guessed = {
|
||||
stackName: hintedStackName,
|
||||
resourceId: hintedResourceId,
|
||||
sourcePaths: attempts.map(a => a.path),
|
||||
apiReachable: attempts.length > 0,
|
||||
hints: {
|
||||
mentionsOpenclaw: /openclaw/.test(textBlob),
|
||||
mentionsKomodo: /komodo/.test(textBlob),
|
||||
mentionsCompose: /compose/.test(textBlob),
|
||||
},
|
||||
matchedObjects: candidates.slice(0, 10).map(obj => {
|
||||
const entries = Object.entries(obj)
|
||||
.filter(([k, v]) => !secretish.test(k) && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
|
||||
.slice(0, 20);
|
||||
return Object.fromEntries(entries);
|
||||
}),
|
||||
};
|
||||
|
||||
return guessed;
|
||||
}
|
||||
|
||||
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 + optional Komodo API (sanitized)',
|
||||
container: {
|
||||
name: (c.Name || '').replace(/^\//, ''),
|
||||
idShort: short(c.Id, 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: safeLabels(config.Labels),
|
||||
komodo: {
|
||||
status: komodoStatus,
|
||||
...summarizeKomodo(komodoRaw),
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(outFile, JSON.stringify(summary, null, 2));
|
||||
console.log(`Wrote sanitized runtime facts to ${outFile}`);
|
||||
NODE
|
||||
Reference in New Issue
Block a user