mirror of
https://github.com/openwrt/luci.git
synced 2026-04-15 19:01:56 +00:00
This is a complete rewrite of the original Lua dockerman in ECMAScript and ucode. Now with most of the Lua gone, we can rename LuCI to JUCI. JavaScript ucode Configuration Interface :) Docker manager basically saw no updates or bug fixes due to the Lua update embargo and transition to ECMAScript in the luci repo. But now that the app is rewritten, updates should come readily from the community. Expect a few bugs in this, although it has seen lots of testing - it's also seen lots of development in different directions. Networking scenarios might require some additions and fixes to the GUI. Swarm functionality is not implemented in this client and is left as an exercise to the community and those with time. All functionality found in the original Lua version is present in this one, except for container "upgrade". Some minor differences are introduced to improve layout and logic. There is no "remote endpoint" any longer since sockets are the main method of connecting to dockerd - and sockets accept remote connections. Docker manager and dockerd on the same host are a remote connection. Buuut, dockerd removes listening on any IP without --tls* options after v27. There is no encryption between docker manager and the API endpoint, or the container consoles when using the standard /var/run/docker.sock. See: https://github.com/openwrt/luci/issues/7310 TODO: handle image update ("Upgrade") for a container Signed-off-by: Paul Donald <newtwen+github@gmail.com>
508 lines
20 KiB
Ucode
Executable File
508 lines
20 KiB
Ucode
Executable File
#!/usr/bin/env ucode
|
|
|
|
// Copyright 2025 Paul Donald / luci-lib-docker-js
|
|
// Licensed to the public under the Apache License 2.0.
|
|
// Built against the docker v1.47 API
|
|
|
|
|
|
'use strict';
|
|
|
|
import * as http from 'luci.http';
|
|
import * as fs from 'fs';
|
|
import * as socket from 'socket';
|
|
import { cursor } from 'uci';
|
|
import * as ds from 'luci.docker_socket';
|
|
|
|
// const cmdline = fs.readfile('/proc/self/cmdline');
|
|
// const args = split(cmdline, '\0');
|
|
const caller = trim(fs.readfile('/proc/self/comm'));
|
|
|
|
const BLOCKSIZE = 8192;
|
|
const POLL_TIMEOUT = 8000; // default; can be overridden per request
|
|
// const API_VER = '/v1.47';
|
|
const PROTOCOL = 'HTTP/1.1';
|
|
const CLIENT_VER = '1';
|
|
|
|
function merge(a, b) {
|
|
let c = {};
|
|
for (let k, v in a)
|
|
c[k] = v;
|
|
for (let k, v in b)
|
|
c[k] = v;
|
|
return c;
|
|
};
|
|
|
|
function chunked_body_reader(sock, initial_buffer) {
|
|
let state = 0, chunklen = 0, buffer = initial_buffer || '';
|
|
|
|
function poll_and_recv() {
|
|
let ready = socket.poll(POLL_TIMEOUT, [sock, socket.POLLIN]);
|
|
if (!ready || !length(ready)) return null;
|
|
let data = sock.recv(BLOCKSIZE);
|
|
if (!data) return null;
|
|
buffer += data;
|
|
return true;
|
|
}
|
|
|
|
return () => {
|
|
while (true) {
|
|
if (state === 0) {
|
|
let m = match(buffer, /^([0-9a-fA-F]+)\r\n/);
|
|
if (!m || length(m) < 2) {
|
|
if (!poll_and_recv()) return null;
|
|
continue;
|
|
}
|
|
chunklen = int(m[1], 16);
|
|
buffer = substr(buffer, length(m[0]));
|
|
if (chunklen === 0) return null;
|
|
state = 1;
|
|
}
|
|
if (state === 1 && length(buffer) >= chunklen + 2) {
|
|
let chunk = substr(buffer, 0, chunklen);
|
|
buffer = substr(buffer, chunklen + 2);
|
|
state = 0;
|
|
return chunk;
|
|
} else {
|
|
if (!poll_and_recv()) return null;
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
function read_http_headers(response_headers, response) {
|
|
const lines = split(response, /\r?\n/);
|
|
|
|
for (let l in lines) {
|
|
let kv = match(l, /([^:]+):\s*(.*)/);
|
|
if (kv && length(kv) === 3)
|
|
response_headers[lc(kv[1])] = kv[2];
|
|
}
|
|
|
|
return response_headers;
|
|
};
|
|
|
|
function get_api_ver() {
|
|
|
|
const ctx = cursor();
|
|
const version = ctx.get('dockerd', 'globals', 'api_version') || '';
|
|
const version_str = version ? `/${version}` : '';
|
|
ctx.unload();
|
|
|
|
return version_str;
|
|
};
|
|
|
|
function coerce_values_to_string(obj) {
|
|
for (let k, v in obj) {
|
|
v = `${v}`;
|
|
obj[k]=v;
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
function call_docker(method, path, options) {
|
|
options = options || {};
|
|
const headers = options.headers || {};
|
|
let payload = options.payload || null;
|
|
|
|
/* requires ucode 2026-01-16 if get_socket_dest() provides ip:port e.g.
|
|
'127.0.0.1:2375'.
|
|
We use get_socket_dest_compat() which builds the SockAddress manually to
|
|
avoid this.
|
|
|
|
Important: dockerd after v28 won't accept tcp://x.x.x.x:2375 without
|
|
--tls* options.
|
|
|
|
A solution is a reverse proxy or ssh port forwarding to a remote host that
|
|
uses the unix socket, and you still connect to a 'local port', or socket:
|
|
ssh -L /tmp/docker.sock:localhost:2375 user@remote-host (openssh-client)
|
|
or (dropbear)
|
|
socat TCP-LISTEN:12375,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock
|
|
ssh -L 2375:localhost:12375 user@remote-host
|
|
*/
|
|
|
|
|
|
/* works on ucode 2025-12-01 */
|
|
const sock_dest = ds.get_socket_dest_compat();
|
|
const sock = socket.create(sock_dest.family, socket.SOCK_STREAM);
|
|
|
|
/* works on ucode 2026-01-16 */
|
|
// const sock_dest = ds.get_socket_dest();
|
|
// const sock_addr = socket.sockaddr(sock_dest);
|
|
// const sock = socket.create(sock_addr.family, socket.SOCK_STREAM);
|
|
|
|
if (caller != 'rpcd') {
|
|
print('sock_dest:', sock_dest, '\n');
|
|
// print('sock_addr:', sock_addr, '\n');
|
|
}
|
|
if (!sock) {
|
|
return {
|
|
code: 500,
|
|
headers: {},
|
|
body: { message: "Failed to create socket" }
|
|
};
|
|
}
|
|
|
|
let conn_result = sock.connect(sock_dest);
|
|
let err_msg = `Failed to connect to docker host at ${sock_dest}`;
|
|
if (!conn_result) {
|
|
sock.close();
|
|
return {
|
|
code: 500,
|
|
headers: {},
|
|
body: { message: err_msg}
|
|
};
|
|
}
|
|
|
|
if (caller != 'rpcd')
|
|
print("query: ", options.query, '\n');
|
|
|
|
const query = options.query ? http.build_querystring(coerce_values_to_string(options.query)) : '';
|
|
const url = path + query;
|
|
|
|
const req_headers = [
|
|
`${method} ${get_api_ver()}${url} ${PROTOCOL}`,
|
|
`Host: luci-host`,
|
|
`User-Agent: luci-app-dockerman-rpc-ucode/${CLIENT_VER}`,
|
|
`Connection: close`
|
|
];
|
|
|
|
if (payload) {
|
|
if (type(payload) === 'object') {
|
|
payload = sprintf('%J', payload);
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
headers['Content-Length'] = '' + length(payload);
|
|
}
|
|
|
|
for (let k, v in headers)
|
|
push(req_headers, `${k}: ${v}`);
|
|
|
|
push(req_headers, '', '');
|
|
|
|
if (caller != 'rpcd')
|
|
print(join('\r\n', req_headers), "\n");
|
|
|
|
sock.send(join('\r\n', req_headers));
|
|
if (payload) sock.send(payload);
|
|
|
|
const response_buff = sock.recv(BLOCKSIZE);
|
|
if (!response_buff || response_buff === '') {
|
|
sock.close();
|
|
return {
|
|
code: 500,
|
|
headers: {},
|
|
body: { message: "No response from Docker socket" }
|
|
};
|
|
}
|
|
|
|
const response_parts = split(response_buff, /\r?\n\r?\n/, 2);
|
|
const response_headers = read_http_headers({}, response_parts[0]);
|
|
let response_body;
|
|
|
|
let is_chunked = (response_headers['transfer-encoding'] === 'chunked');
|
|
|
|
let reader;
|
|
if (is_chunked) {
|
|
reader = chunked_body_reader(sock, response_parts[1]);
|
|
}
|
|
else if (response_headers['content-length']) {
|
|
let content_length = int(response_headers['content-length']);
|
|
let buf = response_parts[1];
|
|
|
|
reader = () => {
|
|
if (content_length <= 0) return null;
|
|
|
|
if (buf && length(buf)) {
|
|
let chunk = substr(buf, 0, content_length);
|
|
buf = substr(buf, length(chunk));
|
|
content_length -= length(chunk);
|
|
return chunk;
|
|
}
|
|
|
|
let data = sock.recv(min(BLOCKSIZE, content_length));
|
|
if (!data || data === '') return null;
|
|
|
|
content_length -= length(data);
|
|
return data;
|
|
};
|
|
}
|
|
else {
|
|
// Fallback for HTTP/1.0 or no content-length: read until close or timeout
|
|
reader = () => {
|
|
// Poll with 2 second timeout
|
|
let ready = socket.poll(POLL_TIMEOUT, [sock, socket.POLLIN]);
|
|
if (!ready || !length(ready)) return null; // Timeout or error
|
|
|
|
let data = sock.recv(BLOCKSIZE);
|
|
if (!data || data === '') return null;
|
|
return data;
|
|
};
|
|
}
|
|
|
|
let chunks = [], chunk;
|
|
while ((chunk = reader())) {
|
|
push(chunks, chunk);
|
|
}
|
|
|
|
sock.close();
|
|
|
|
response_body = join('', chunks);
|
|
|
|
// Parse HTTP status code
|
|
let status_line = split(response_parts[0], /\r?\n/)[0];
|
|
let status_match = match(status_line, /HTTP\/\S+\s+(\d+)/);
|
|
let code = status_match ? int(status_match[1]) : 0;
|
|
|
|
// Docker events endpoint returns newline-delimited JSON, not a single JSON object
|
|
if (response_headers['content-type'] === 'application/json' && response_body) {
|
|
// Single JSON object
|
|
let data;
|
|
try { data = json(rtrim(response_body)); }
|
|
catch { data = null; }
|
|
|
|
// Check if this is newline-delimited JSON (multiple lines with JSON objects)
|
|
if (!data) {
|
|
// Parse each line as a separate JSON object
|
|
let lines = split(trim(response_body), /\n/);
|
|
let events = [];
|
|
for (let line in lines) {
|
|
line = trim(line);
|
|
if (line) {
|
|
try { push(events, json(line)); }
|
|
catch { /* skip invalid lines */ }
|
|
}
|
|
}
|
|
response_body = events;
|
|
} else {
|
|
response_body = data;
|
|
}
|
|
}
|
|
|
|
return {
|
|
code: code,
|
|
headers: response_headers,
|
|
body: response_body
|
|
};
|
|
};
|
|
|
|
function run_ttyd(request) {
|
|
|
|
const id = request.args.id || '';
|
|
const cmd = request.args.cmd || '/bin/sh';
|
|
const port = request.args.port || 7682;
|
|
const uid = request.args.uid || '';
|
|
|
|
if (!id) {
|
|
return { error: 'Container ID is required' };
|
|
}
|
|
|
|
let ttyd_cmd = `ttyd -q -d 2 --once --writable -p ${port} docker`;
|
|
const sock_addr = ds.get_socket_dest();
|
|
|
|
/* Build the full command:
|
|
ttyd --writable -d 2 --once -p PORT docker -H unix://SOCKET exec -it [-u UID] CONTAINER CMD
|
|
|
|
if the socket is /var/run/docker.sock, prefix unix://
|
|
|
|
Note: invocations of docker -H x.x.x.x:2375 [..] will fail after v27 without --tls*
|
|
*/
|
|
const sock_str = index(sock_addr, '/') != -1 && index(sock_addr, 'unix://') == -1 ? 'unix://' + sock_addr : sock_addr;
|
|
ttyd_cmd = `${ttyd_cmd} -H "${sock_str}" exec -it`;
|
|
if (uid && uid !== '') {
|
|
ttyd_cmd = `${ttyd_cmd} -u ${uid}`;
|
|
}
|
|
|
|
ttyd_cmd = `${ttyd_cmd} ${id} ${cmd} &`;
|
|
|
|
// Try to kill any existing ttyd processes on this port
|
|
system(`pkill -f "ttyd.*-p ${port}"` + ' 2>/dev/null; true');
|
|
|
|
// Start ttyd
|
|
system(ttyd_cmd);
|
|
|
|
return { status: 'ttyd started', command: ttyd_cmd };
|
|
}
|
|
|
|
// https://docs.docker.com/reference/api/engine/version/v1.47/
|
|
|
|
/* Note: methods here are included for structural reference. Some rpcd methods
|
|
are not suitable to be called from the GUI because they are streaming endpoints
|
|
or the operations in a busy dockerd cluster take a *long* time which causes
|
|
timeouts at the front end. Good examples of this are:
|
|
- /system/df
|
|
- push
|
|
- pull
|
|
- all /prune
|
|
|
|
We include them here because they can be useful from the command line.
|
|
*/
|
|
|
|
const core_methods = {
|
|
version: { call: () => call_docker('GET', '/version') },
|
|
info: { call: () => call_docker('GET', '/info') },
|
|
ping: { call: () => call_docker('GET', '/_ping') },
|
|
df: { call: () => call_docker('GET', '/system/df') },
|
|
events: { args: { query: { 'since': '', 'until': `${time()}`, 'filters': '' } }, call: (request) => call_docker('GET', '/events', { query: request?.args?.query }) },
|
|
};
|
|
|
|
const exec_methods = {
|
|
start: { args: { id: '', body: '' }, call: (request) => call_docker('POST', `/exec/${request.args.id}/start`, { payload: request.args.body }) },
|
|
resize: { args: { id: '', query: { 'h': 0, 'w': 0 } }, call: (request) => call_docker('POST', `/exec/${request.args.id}/resize`, { query: request.args.query }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/exec/${request.args.id}/json`) },
|
|
};
|
|
|
|
const container_methods = {
|
|
list: { args: { query: { 'all': false, 'limit': false, 'size': false, 'filters': '' } }, call: (request) => call_docker('GET', '/containers/json', { query: request.args.query }) },
|
|
create: { args: { query: { 'name': '', 'platform': '' }, body: {} }, call: (request) => call_docker('POST', '/containers/create', { query: request.args.query, payload: request.args.body }) },
|
|
inspect: { args: { id: '', query: { 'size': false } }, call: (request) => call_docker('GET', `/containers/${request.args.id}/json`, { query: request.args.query }) },
|
|
top: { args: { id: '', query: { 'ps_args': '' } }, call: (request) => call_docker('GET', `/containers/${request.args.id}/top`, { query: request.args.query }) },
|
|
logs: { args: { id: '', query: {} }, call: (request) => call_docker('GET', `/containers/${request.args.id}/logs`, { query: request.args.query }) },
|
|
changes: { args: { id: '' }, call: (request) => call_docker('GET', `/containers/${request.args.id}/changes`) },
|
|
export: { args: { id: '' }, call: (request) => call_docker('GET', `/containers/${request.args.id}/export`) },
|
|
stats: { args: { id: '', query: { 'stream': false, 'one-shot': false } }, call: (request) => call_docker('GET', `/containers/${request.args.id}/stats`, { query: request.args.query }) },
|
|
resize: { args: { id: '', query: { 'h': 0, 'w': 0 } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/resize`, { query: request.args.query }) },
|
|
start: { args: { id: '', query: { 'detachKeys': '' } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/start`, { query: request.args.query }) },
|
|
stop: { args: { id: '', query: { 'signal': '', 't': 0 } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/stop`, { query: request.args.query }) },
|
|
restart: { args: { id: '', query: { 'signal': '', 't': 0 } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/restart`, { query: request.args.query }) },
|
|
kill: { args: { id: '', query: { 'signal': '' } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/kill`, { query: request.args.query }) },
|
|
update: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/containers/${request.args.id}/update`, { payload: request.args.body }) },
|
|
rename: { args: { id: '', query: { 'name': '' } }, call: (request) => call_docker('POST', `/containers/${request.args.id}/rename`, { query: request.args.query }) },
|
|
pause: { args: { id: '' }, call: (request) => call_docker('POST', `/containers/${request.args.id}/pause`) },
|
|
unpause: { args: { id: '' }, call: (request) => call_docker('POST', `/containers/${request.args.id}/unpause`) },
|
|
// attach
|
|
// attach websocket
|
|
// wait
|
|
remove: { args: { id: '', query: { 'v': false, 'force': false, 'link': false } }, call: (request) => call_docker('DELETE', `/containers/${request.args.id}`, { query: request.args.query }) },
|
|
// archive info
|
|
info_archive: { args: { id: '', query: { 'path': '' } }, call: (request) => call_docker('HEAD', `/containers/${request.args.id}/archive`, { query: request.args.query }) },
|
|
// archive get
|
|
get_archive: { args: { id: '', query: { 'path': '' } }, call: (request) => call_docker('GET', `/containers/${request.args.id}/archive`, { query: request.args.query }) },
|
|
// archive extract
|
|
put_archive: { args: { id: '', query: { 'path': '', 'noOverwriteDirNonDir': '', 'copyUIDGID': '' }, body: '' }, call: (request) => call_docker('PUT', `/containers/${request.args.id}/archive`, { query: request.args.query, payload: request.args.body }) },
|
|
exec: { args: { id: '', opts: {} }, call: (request) => call_docker('POST', `/containers/${request.args.id}/exec`, { payload: request.args.opts }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/containers/prune', { query: request.args.query }) },
|
|
|
|
// Not a docker command - but a local command to invoke ttyd so our browser can open websocket to docker
|
|
ttyd_start: { args: { id: '', cmd: '/bin/sh', port: 7682, uid: '' }, call: (request) => run_ttyd(request) },
|
|
};
|
|
|
|
const image_methods = {
|
|
list: { args: { query: { 'all': false, 'digests': false, 'shared-size': false, 'manifests': false, 'filters': '' } }, call: (request) => call_docker('GET', '/images/json', { query: request.args.query }) },
|
|
// build is long-running, and will likely cause time-out on the call. Function only here for reference.
|
|
build: { args: { query: { '': '' }, headers: {} }, call: (request) => call_docker('POST', '/build', { query: request.args.query, headers: request.args.headers }) },
|
|
build_prune: { args: { query: { '': '' }, headers: {} }, call: (request) => call_docker('POST', '/build/prune', { query: request.args.query, headers: request.args.headers }) },
|
|
create: { args: { query: { '': '' }, headers: {} }, call: (request) => call_docker('POST', '/images/create', { query: request.args.query, headers: request.args.headers }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/images/${request.args.id}/json`) },
|
|
history: { args: { id: '' }, call: (request) => call_docker('GET', `/images/${request.args.id}/history`) },
|
|
push: { args: { name: '', query: { tag: '', platform: '' }, headers: {} }, call: (request) => call_docker('POST', `/images/${request.args.name}/push`, { query: request.args.query, headers: request.args.headers }) },
|
|
tag: { args: { id: '', query: { 'repo': '', 'tag': '' } }, call: (request) => call_docker('POST', `/images/${request.args.id}/tag`, { query: request.args.query }) },
|
|
remove: { args: { id: '', query: { 'force': false, 'noprune': false } }, call: (request) => call_docker('DELETE', `/images/${request.args.id}`, { query: request.args.query }) },
|
|
search: { args: { query: { 'term': '', 'limit': 0, 'filters': '' } }, call: (request) => call_docker('GET', '/images/search', { query: request.args.query }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/images/prune', { query: request.args.query }) },
|
|
// create/commit
|
|
get: { args: { id: '' }, call: (request) => call_docker('GET', `/images/${request.args.id}/get`) },
|
|
// get == export several
|
|
load: { args: { query: { 'quiet': false } }, call: (request) => call_docker('POST', '/images/load', { query: request.args.query }) },
|
|
};
|
|
|
|
const network_methods = {
|
|
list: { args: { query: { 'filters': '' } }, call: (request) => call_docker('GET', '/networks', { query: request.args.query }) },
|
|
inspect: { args: { id: '', query: { 'verbose': false, 'scope': '' } }, call: (request) => call_docker('GET', `/networks/${request.args.id}`, { query: request.args.query }) },
|
|
remove: { args: { id: '' }, call: (request) => call_docker('DELETE', `/networks/${request.args.id}`) },
|
|
create: { args: { body: {} }, call: (request) => call_docker('POST', '/networks/create', { payload: request.args.body }) },
|
|
connect: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/networks/${request.args.id}/connect`, { payload: request.args.body }) },
|
|
disconnect: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/networks/${request.args.id}/disconnect`, { payload: request.args.body }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/networks/prune', { query: request.args.query }) },
|
|
};
|
|
|
|
const volume_methods = {
|
|
list: { args: { query: { 'filters': '' } }, call: (request) => call_docker('GET', '/volumes', { query: request.args.query }) },
|
|
create: { args: { opts: {} }, call: (request) => call_docker('POST', '/volumes/create', { payload: request.args.opts }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/volumes/${request.args.id}`) },
|
|
update: { args: { id: '', query: { 'version': 0 }, spec: {} }, call: (request) => call_docker('PUT', `/volumes/${request.args.id}`, { query: request.args.query, payload: request.args.spec }) },
|
|
remove: { args: { id: '', query: { 'force': false } }, call: (request) => call_docker('DELETE', `/volumes/${request.args.id}`, { query: request.args.query }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/volumes/prune', { query: request.args.query }) },
|
|
};
|
|
|
|
const methods = {
|
|
'docker': core_methods,
|
|
'docker.container': container_methods,
|
|
'docker.exec': exec_methods,
|
|
'docker.image': image_methods,
|
|
'docker.network': network_methods,
|
|
'docker.volume': volume_methods,
|
|
};
|
|
|
|
// CLI test mode - check if script is run directly (not loaded by rpcd)
|
|
if (caller != 'rpcd') {
|
|
// Usage: ./docker_rpc.uc <object.method> <json-args>
|
|
// Example: ./docker_rpc.uc docker.network.list '{"query":{"filters":""}}'
|
|
// Example: ./docker_rpc.uc docker.image.create '{"query":{"fromImage":"alpine","tag":"latest"}}'
|
|
const scr_name = split(SCRIPT_NAME, '/')[-1] || 'docker_rpc.uc';
|
|
|
|
if (length(ARGV) < 1) {
|
|
print(`Usage: ${scr_name} <object.method> [json-args]\n`);
|
|
|
|
print("Available methods:\n");
|
|
for (let obj in methods) {
|
|
for (let name, info in methods[obj]) {
|
|
let sig = name;
|
|
if (info && info.args) {
|
|
try {
|
|
sig = `${sig} ${sprintf('\'%J\'', info.args)}`;
|
|
} catch {
|
|
sig = `${sig} <args>`;
|
|
}
|
|
}
|
|
print(` ${obj}.${sig}\n`);
|
|
}
|
|
}
|
|
|
|
print("\nExamples:\n");
|
|
print(` ${scr_name} docker.version\n`);
|
|
print(` ${scr_name} docker.network.list '{"query":{}}'\n`);
|
|
print(` ${scr_name} docker.image.create '{"query":{"fromImage":"alpine","tag":"latest"}}'\n`);
|
|
print(` ${scr_name} docker.container.list '{"query":{"all":true}}'\n`);
|
|
exit(1);
|
|
}
|
|
|
|
const method_path = split(ARGV[0], '.');
|
|
if (length(method_path) < 1) {
|
|
die(`Invalid method path: ${ARGV[0]}\n`);
|
|
}
|
|
|
|
// Build object path (e.g., "docker.network")
|
|
const obj_parts = slice(method_path, 0, -1);
|
|
const obj_name = join('.', obj_parts);
|
|
const method_name = method_path[length(method_path) - 1];
|
|
|
|
if (!methods[obj_name]) {
|
|
die(`Unknown object: ${obj_name}\n`);
|
|
}
|
|
|
|
if (!methods[obj_name][method_name]) {
|
|
die(`Unknown method: ${obj_name}.${method_name}\n`);
|
|
}
|
|
|
|
// Parse args if provided
|
|
let args = {};
|
|
if (length(ARGV) > 1) {
|
|
try {
|
|
args = json(ARGV[1]);
|
|
} catch (e) {
|
|
die(`Invalid JSON args: ${e}\n`);
|
|
}
|
|
}
|
|
|
|
// Call the method
|
|
const request = { args: args };
|
|
const result = methods[obj_name][method_name].call(request);
|
|
|
|
// Pretty print result
|
|
print(result, "\n");
|
|
exit(0);
|
|
};
|
|
|
|
return methods;
|