fb4454678a
Signed-off-by: sbwml <admin@cooluc.com>
535 lines
21 KiB
JavaScript
535 lines
21 KiB
JavaScript
'use strict';
|
|
'require rpc';
|
|
'require uci';
|
|
|
|
/*
|
|
Copyright 2026
|
|
Docker manager JS for Luci by Paul Donald <newtwen+github@gmail.com>
|
|
LICENSE: GPLv2.0
|
|
*/
|
|
|
|
const callNetworkInterfaceDump = rpc.declare({
|
|
object: 'network.interface',
|
|
method: 'dump',
|
|
expect: { 'interface': [] }
|
|
});
|
|
|
|
|
|
let dockerHosts = null;
|
|
let dockerHost = null;
|
|
let localIPv4 = null;
|
|
let localIPv6 = null;
|
|
let js_api_available = false;
|
|
|
|
|
|
// Load both UCI config and network interfaces in parallel
|
|
const loadPromise = Promise.all([
|
|
callNetworkInterfaceDump(),
|
|
uci.load('dockerd'),
|
|
]).then(([interfaceData]) => {
|
|
|
|
const lan_device = uci.get('dockerd', 'globals', '_luci_lan') || 'lan';
|
|
|
|
// Find local IPs from network interfaces
|
|
if (interfaceData) {
|
|
interfaceData.forEach(iface => {
|
|
// console.log(iface.up)
|
|
if (!iface.up || iface.interface !== lan_device) return;
|
|
|
|
// Get IPv4 address
|
|
if (!localIPv4 && iface['ipv4-address']) {
|
|
const addr4 = iface['ipv4-address'].find(a =>
|
|
a.address && !a.address.startsWith('127.')
|
|
);
|
|
if (addr4) localIPv4 = addr4.address;
|
|
}
|
|
|
|
// Get IPv6 address
|
|
if (!localIPv6) {
|
|
// Try ipv6-address array first
|
|
if (iface['ipv6-address']) {
|
|
const addr6 = iface['ipv6-address'].find(a =>
|
|
a.address && a.address !== '::1' && !a.address.startsWith('fe80:')
|
|
);
|
|
if (addr6) localIPv6 = addr6.address;
|
|
}
|
|
|
|
// Try ipv6-prefix-assignment if no address found
|
|
if (!localIPv6 && iface['ipv6-prefix-assignment']) {
|
|
const prefix = iface['ipv6-prefix-assignment'].find(p =>
|
|
p['local-address'] && p['local-address'].address
|
|
);
|
|
if (prefix) localIPv6 = prefix['local-address'].address;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
dockerHosts = uci.get_first('dockerd', 'globals', 'hosts');
|
|
|
|
// Find and convert first tcp:// or tcp6:// host
|
|
const hostsList = Array.isArray(dockerHosts) ? dockerHosts : [];
|
|
const dh = hostsList.find(h => h
|
|
&& (h.startsWith('tcp://')
|
|
|| h.startsWith('tcp6://')
|
|
|| h.startsWith('inet6://')
|
|
|| h.startsWith('http://')
|
|
|| h.startsWith('https://')
|
|
));
|
|
|
|
if (dh) {
|
|
// const isTcp6 = dh.startsWith('tcp6://');
|
|
const protocol = dh.includes(':2376') ? 'https://' : 'http://';
|
|
dockerHost = dh.replace(/^(tcp|inet)6?:\/\//, protocol);
|
|
|
|
// Replace 0.0.0.0 or :: with appropriate local IP
|
|
if (localIPv6) {
|
|
dockerHost = dockerHost.replace(/\[::1?\]/, `[${localIPv6}]`);
|
|
// dockerHost = dockerHost.replace(/::/, localIPv6);
|
|
}
|
|
|
|
if (localIPv4) {
|
|
dockerHost = dockerHost.replace(/0\.0\.0\.0/, localIPv4);
|
|
}
|
|
|
|
console.log('Docker configured to use JS API to:', dockerHost);
|
|
}
|
|
|
|
return dockerHost;
|
|
});
|
|
|
|
|
|
// Helper to process NDJSON or line-delimited JSON chunks
|
|
function processLines(buffer, onChunk) {
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
for (const line of lines) {
|
|
if (line.trim()) {
|
|
try {
|
|
const json = JSON.parse(line);
|
|
onChunk(json);
|
|
} catch (e) {
|
|
onChunk({ raw: line });
|
|
}
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
|
|
function call_docker(method, path, options = {}) {
|
|
return loadPromise.then(() => {
|
|
const headers = { ...(options.headers || {}) };
|
|
const payload = options.payload || null;
|
|
const query = options.query || null;
|
|
const host = dockerHost;
|
|
const onChunk = options.onChunk || null; // Optional callback for streaming NDJSON
|
|
const api_ver = uci.get('dockerd', 'globals', 'api_version') || '';
|
|
const api_ver_str = api_ver ? `/${api_ver}` : '';
|
|
|
|
|
|
if (!host) {
|
|
return Promise.reject(new Error('Docker host not configured'));
|
|
}
|
|
|
|
// Check if WebSocket upgrade is requested
|
|
const isWebSocketUpgrade = headers['Connection']?.toLowerCase() === 'upgrade' ||
|
|
headers['connection']?.toLowerCase() === 'upgrade';
|
|
|
|
if (isWebSocketUpgrade) {
|
|
return createWebSocketConnection(host, path, query);
|
|
}
|
|
|
|
// Build URL
|
|
let url = `${host}${api_ver_str}${path}`;
|
|
if (query) {
|
|
const params = new URLSearchParams();
|
|
for (const key in query) {
|
|
if (query[key] != null) {
|
|
params.append(key, query[key]);
|
|
}
|
|
}
|
|
|
|
// dockerd does not like encoded params here.
|
|
const queryString = params.toString();
|
|
if (queryString) {
|
|
url += `?${queryString}`;
|
|
}
|
|
}
|
|
|
|
// Build fetch options
|
|
const fetchOptions = {
|
|
method,
|
|
headers: {
|
|
...headers // Always include custom headers
|
|
},
|
|
};
|
|
|
|
if (payload) {
|
|
fetchOptions.body = JSON.stringify(payload);
|
|
if (!fetchOptions.headers['Content-Type']) {
|
|
fetchOptions.headers['Content-Type'] = 'application/json';
|
|
}
|
|
}
|
|
|
|
// Make the request
|
|
return fetch(url, fetchOptions)
|
|
.then(response => {
|
|
// If streaming callback provided, use streaming response
|
|
if (onChunk) {
|
|
const reader = response.body?.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
return new Promise((resolve) => {
|
|
const processStream = async () => {
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) {
|
|
// Process any remaining data in buffer
|
|
buffer = processLines(buffer, onChunk);
|
|
break;
|
|
}
|
|
// Decode chunk and add to buffer
|
|
buffer += decoder.decode(value, { stream: true });
|
|
// Use generic processor for NDJSON/line chunks
|
|
buffer = processLines(buffer, onChunk);
|
|
}
|
|
|
|
// Return final response
|
|
resolve({
|
|
code: response.status,
|
|
headers: response.headers
|
|
});
|
|
} catch (err) {
|
|
console.error('Streaming error:', err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
processStream();
|
|
});
|
|
}
|
|
|
|
// Normal buffered response
|
|
if (response?.status >= 304) {
|
|
console.error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const headersObj = {};
|
|
for (const [key, value] of response.headers.entries()) {
|
|
headersObj[key] = value;
|
|
}
|
|
|
|
return response.text().then(text => {
|
|
const safeText = (typeof text === 'string') ? text : '';
|
|
let parsed = safeText || text;
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
// Try normal JSON parse first
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch (err) {
|
|
// If the payload is newline-delimited JSON (Docker events), split and parse each line
|
|
if (['application/json',
|
|
'application/x-ndjson',
|
|
'application/json-seq'].includes(contentType) || safeText.includes('\n')) {
|
|
const lines = safeText.split(/\r?\n/).filter(Boolean);
|
|
try {
|
|
parsed = lines.map(l => JSON.parse(l));
|
|
} catch (err2) {
|
|
// Fall back to raw text if parsing fails
|
|
parsed = text;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
code: response.status,
|
|
body: parsed,
|
|
headers: headersObj,
|
|
};
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Docker API error:', error);
|
|
});
|
|
});
|
|
}
|
|
|
|
function createWebSocketConnection(host, path, query) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// Convert http/https to ws/wss
|
|
const wsHost = host
|
|
.replace(/^https:/, 'wss:')
|
|
.replace(/^http:/, 'ws:');
|
|
|
|
// Build WebSocket URL
|
|
let wsUrl = `${wsHost}${path}`;
|
|
if (query) {
|
|
const params = new URLSearchParams();
|
|
for (const key in query) {
|
|
if (query[key] != null) {
|
|
params.append(key, query[key]);
|
|
}
|
|
}
|
|
const queryString = params.toString();
|
|
if (queryString) {
|
|
wsUrl += `?${queryString}`;
|
|
}
|
|
}
|
|
|
|
console.log('Opening WebSocket connection to:', wsUrl);
|
|
|
|
const ws = new WebSocket(wsUrl);
|
|
let resolved = false;
|
|
|
|
// Handle connection open
|
|
ws.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
if (!resolved) {
|
|
resolved = true;
|
|
// Return a Response-like object with WebSocket support
|
|
resolve({
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: new Map(),
|
|
body: ws,
|
|
ws: ws,
|
|
// Add helper for sending messages
|
|
send: (data) => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(data);
|
|
}
|
|
},
|
|
// Add helper for receiving messages as async iterator
|
|
async *[Symbol.asyncIterator]() {
|
|
while (ws.readyState === WebSocket.OPEN) {
|
|
yield new Promise((res, rej) => {
|
|
const messageHandler = (event) => {
|
|
ws.removeEventListener('message', messageHandler);
|
|
ws.removeEventListener('error', errorHandler);
|
|
res(event.data);
|
|
};
|
|
const errorHandler = (error) => {
|
|
ws.removeEventListener('message', messageHandler);
|
|
ws.removeEventListener('error', errorHandler);
|
|
rej(error);
|
|
};
|
|
ws.addEventListener('message', messageHandler);
|
|
ws.addEventListener('error', errorHandler);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle connection error
|
|
ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
if (!resolved) {
|
|
resolved = true;
|
|
reject(new Error(`WebSocket connection failed: ${error.message || 'Unknown error'}`));
|
|
}
|
|
};
|
|
|
|
// Handle close (including handshake failures)
|
|
ws.onclose = (event) => {
|
|
console.log('WebSocket closed');
|
|
if (!resolved) {
|
|
resolved = true;
|
|
reject(new Error(`WebSocket closed before open (${event?.code || 'unknown'})`));
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
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': `${Date.now()}`, 'filters': '' } }, call: (request) => call_docker('GET', '/events', { query: request?.query, onChunk: request?.onChunk }) },
|
|
};
|
|
|
|
/*
|
|
const exec_methods = {
|
|
start: { args: { id: '', body: '' }, call: (request) => call_docker('POST', `/exec/${request?.id}/start`, { payload: request?.body }) },
|
|
resize: { args: { id: '', query: { 'h': 0, 'w': 0 } }, call: (request) => call_docker('POST', `/exec/${request?.id}/resize`, { query: request?.query }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/exec/${request?.id}/json`) },
|
|
};
|
|
*/
|
|
|
|
const container_methods = {
|
|
list: { args: { query: { 'all': false, 'limit': false, 'size': false, 'filters': '' } }, call: (request) => call_docker('GET', '/containers/json', { query: request?.query }) },
|
|
create: { args: { query: { 'name': '', 'platform': '' }, body: {} }, call: (request) => call_docker('POST', '/containers/create', { query: request?.query, payload: request?.body }) },
|
|
inspect: { args: { id: '', query: { 'size': false } }, call: (request) => call_docker('GET', `/containers/${request?.id}/json`, { query: request?.query }) },
|
|
top: { args: { id: '', query: { 'ps_args': '' } }, call: (request) => call_docker('GET', `/containers/${request?.id}/top`, { query: request?.query }) },
|
|
logs: { args: { id: '', query: {} }, call: (request) => call_docker('GET', `/containers/${request?.id}/logs`, { query: request?.query, onChunk: request?.onChunk }) },
|
|
changes: { args: { id: '' }, call: (request) => call_docker('GET', `/containers/${request?.id}/changes`) },
|
|
export: { args: { id: '' }, call: (request) => call_docker('GET', `/containers/${request?.id}/export`) },
|
|
stats: { args: { id: '', query: { 'stream': false, 'one-shot': false } }, call: (request) => call_docker('GET', `/containers/${request?.id}/stats`, { query: request?.query }) },
|
|
resize: { args: { id: '', query: { 'h': 0, 'w': 0 } }, call: (request) => call_docker('POST', `/containers/${request?.id}/resize`, { query: request?.query }) },
|
|
start: { args: { id: '', query: { 'detachKeys': '' } }, call: (request) => call_docker('POST', `/containers/${request?.id}/start`, { query: request?.query }) },
|
|
stop: { args: { id: '', query: { 'signal': '', 't': 0 } }, call: (request) => call_docker('POST', `/containers/${request?.id}/stop`, { query: request?.query }) },
|
|
restart: { args: { id: '', query: { 'signal': '', 't': 0 } }, call: (request) => call_docker('POST', `/containers/${request?.id}/restart`, { query: request?.query }) },
|
|
kill: { args: { id: '', query: { 'signal': '' } }, call: (request) => call_docker('POST', `/containers/${request?.id}/kill`, { query: request?.query }) },
|
|
update: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/containers/${request?.id}/update`, { payload: request?.body }) },
|
|
rename: { args: { id: '', query: { 'name': '' } }, call: (request) => call_docker('POST', `/containers/${request?.id}/rename`, { query: request?.query }) },
|
|
pause: { args: { id: '' }, call: (request) => call_docker('POST', `/containers/${request?.id}/pause`) },
|
|
unpause: { args: { id: '' }, call: (request) => call_docker('POST', `/containers/${request?.id}/unpause`) },
|
|
// attach
|
|
// attach websocket
|
|
attach_ws: { args: { id: '' }, call: (request) => call_docker('GET', `/containers/${request?.id}/attach/ws`, { query: request?.query, headers: { 'Connection': 'Upgrade' } }) },
|
|
// wait
|
|
remove: { args: { id: '', query: { 'v': false, 'force': false, 'link': false } }, call: (request) => call_docker('DELETE', `/containers/${request?.id}`, { query: request?.query }) },
|
|
// archive info
|
|
info_archive: { args: { id: '', query: { 'path': '' } }, call: (request) => call_docker('HEAD', `/containers/${request?.id}/archive`, { query: request?.query }) },
|
|
// archive get
|
|
get_archive: { args: { id: '', query: { 'path': '' } }, call: (request) => call_docker('GET', `/containers/${request?.id}/archive`, { query: request?.query }) },
|
|
// archive extract
|
|
put_archive: { args: { id: '', query: { 'path': '', 'noOverwriteDirNonDir': '', 'copyUIDGID': '' }, body: '' }, call: (request) => call_docker('PUT', `/containers/${request?.id}/archive`, { query: request?.query, payload: request?.body }) },
|
|
exec: { args: { id: '', opts: {} }, call: (request) => call_docker('POST', `/containers/${request?.id}/exec`, { payload: request?.opts }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/containers/prune', { query: request?.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?.query }) },
|
|
build: { args: { query: { '': '' }, headers: {}, onChunk: null }, call: (request) => call_docker('POST', '/build', { query: request?.query, headers: request?.headers, onChunk: request?.onChunk }) },
|
|
build_prune: { args: { query: { '': '' }, headers: {}, onChunk: null }, call: (request) => call_docker('POST', '/build/prune', { query: request?.query, headers: request?.headers, onChunk: request?.onChunk }) },
|
|
create: { args: { query: { '': '' }, headers: {}, onChunk: null }, call: (request) => call_docker('POST', '/images/create', { query: request?.query, headers: request?.headers, onChunk: request?.onChunk }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/images/${request?.id}/json`) },
|
|
history: { args: { id: '' }, call: (request) => call_docker('GET', `/images/${request?.id}/history`) },
|
|
push: { args: { name: '', query: { tag: '', platform: '' }, headers: {}, onChunk: null }, call: (request) => call_docker('POST', `/images/${request?.name}/push`, { query: request?.query, headers: request?.headers, onChunk: request?.onChunk }) },
|
|
tag: { args: { id: '', query: { 'repo': '', 'tag': '' } }, call: (request) => call_docker('POST', `/images/${request?.id}/tag`, { query: request?.query }) },
|
|
remove: { args: { id: '', query: { 'force': false, 'noprune': false }, onChunk: null }, call: (request) => call_docker('DELETE', `/images/${request?.id}`, { query: request?.query, onChunk: request?.onChunk }) },
|
|
search: { args: { query: { 'term': '', 'limit': 0, 'filters': '' } }, call: (request) => call_docker('GET', '/images/search', { query: request?.query }) },
|
|
prune: { args: { query: { 'filters': '' }, onChunk: null }, call: (request) => call_docker('POST', '/images/prune', { query: request?.query, onChunk: request?.onChunk }) },
|
|
// create/commit
|
|
get: { args: { id: '', onChunk: null }, call: (request) => call_docker('GET', `/images/${request?.id}/get`, { onChunk: request?.onChunk }) },
|
|
// get == export several
|
|
load: { args: { query: { 'quiet': false }, onChunk: null }, call: (request) => call_docker('POST', '/images/load', { query: request?.query, onChunk: request?.onChunk }) },
|
|
};
|
|
|
|
|
|
const network_methods = {
|
|
list: { args: { query: { 'filters': '' } }, call: (request) => call_docker('GET', '/networks', { query: request?.query }) },
|
|
inspect: { args: { id: '', query: { 'verbose': false, 'scope': '' } }, call: (request) => call_docker('GET', `/networks/${request?.id}`, { query: request?.query }) },
|
|
remove: { args: { id: '' }, call: (request) => call_docker('DELETE', `/networks/${request?.id}`) },
|
|
create: { args: { body: {} }, call: (request) => call_docker('POST', '/networks/create', { payload: request?.body }) },
|
|
connect: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/networks/${request?.id}/connect`, { payload: request?.body }) },
|
|
disconnect: { args: { id: '', body: {} }, call: (request) => call_docker('POST', `/networks/${request?.id}/disconnect`, { payload: request?.body }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/networks/prune', { query: request?.query }) },
|
|
};
|
|
|
|
|
|
const volume_methods = {
|
|
list: { args: { query: { 'filters': '' } }, call: (request) => call_docker('GET', '/volumes', { query: request?.query }) },
|
|
create: { args: { opts: {} }, call: (request) => call_docker('POST', '/volumes/create', { payload: request?.opts }) },
|
|
inspect: { args: { id: '' }, call: (request) => call_docker('GET', `/volumes/${request?.id}`) },
|
|
update: { args: { id: '', query: { 'version': 0 }, spec: {} }, call: (request) => call_docker('PUT', `/volumes/${request?.id}`, { query: request?.query, payload: request?.spec }) },
|
|
remove: { args: { id: '', query: { 'force': false } }, call: (request) => call_docker('DELETE', `/volumes/${request?.id}`, { query: request?.query }) },
|
|
prune: { args: { query: { 'filters': '' } }, call: (request) => call_docker('POST', '/volumes/prune', { query: request?.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,
|
|
// };
|
|
|
|
|
|
// Determine JS API availability after core methods are ready
|
|
const apiAvailabilityPromise = loadPromise.then(() => {
|
|
if (!dockerHost) {
|
|
js_api_available = false;
|
|
return [js_api_available, dockerHost];
|
|
}
|
|
|
|
return core_methods.ping.call()
|
|
.then(res => {
|
|
// ping returns raw 'OK' text; treat any truthy/OK as success
|
|
const body = res?.body;
|
|
js_api_available = body === 'OK';
|
|
return [js_api_available, dockerHost];
|
|
})
|
|
.catch(error => {
|
|
console.warn('JS API unavailable (likely CORS or network):', error?.message || error);
|
|
js_api_available = false;
|
|
return [js_api_available, dockerHost];
|
|
});
|
|
});
|
|
|
|
|
|
return L.Class.extend({
|
|
js_api_available: () => apiAvailabilityPromise.then(() => [js_api_available, dockerHost]),
|
|
container_attach_ws: container_methods.attach_ws.call,
|
|
container_changes: container_methods.changes.call,
|
|
container_create: container_methods.create.call,
|
|
// container_export: container_export, // use controller instead
|
|
container_info_archive: container_methods.info_archive.call,
|
|
container_inspect: container_methods.inspect.call,
|
|
container_kill: container_methods.kill.call,
|
|
container_list: container_methods.list.call,
|
|
container_logs: container_methods.logs.call,
|
|
container_pause: container_methods.pause.call,
|
|
container_prune: container_methods.prune.call,
|
|
container_remove: container_methods.remove.call,
|
|
container_rename: container_methods.rename.call,
|
|
container_restart: container_methods.restart.call,
|
|
container_start: container_methods.start.call,
|
|
container_stats: container_methods.stats.call,
|
|
container_stop: container_methods.stop.call,
|
|
container_top: container_methods.top.call,
|
|
// container_ttyd_start: container_methods.ttyd_start.call,
|
|
container_unpause: container_methods.unpause.call,
|
|
container_update: container_methods.update.call,
|
|
docker_version: core_methods.version.call,
|
|
docker_info: core_methods.info.call,
|
|
docker_ping: core_methods.ping.call,
|
|
docker_df: core_methods.df.call,
|
|
docker_events: core_methods.events.call,
|
|
image_build: image_methods.build.call,
|
|
image_create: image_methods.create.call,
|
|
image_history: image_methods.history.call,
|
|
image_inspect: image_methods.inspect.call,
|
|
image_list: image_methods.list.call,
|
|
image_prune: image_methods.prune.call,
|
|
image_push: image_methods.push.call,
|
|
image_remove: image_methods.remove.call,
|
|
image_tag: image_methods.tag.call,
|
|
network_connect: network_methods.connect.call,
|
|
network_create: network_methods.create.call,
|
|
network_disconnect: network_methods.disconnect.call,
|
|
network_inspect: network_methods.inspect.call,
|
|
network_list: network_methods.list.call,
|
|
network_prune: network_methods.prune.call,
|
|
network_remove: network_methods.remove.call,
|
|
volume_create: volume_methods.create.call,
|
|
volume_inspect: volume_methods.inspect.call,
|
|
volume_list: volume_methods.list.call,
|
|
volume_prune: volume_methods.prune.call,
|
|
volume_remove: volume_methods.remove.call,
|
|
|
|
});
|
|
|